@gakr-gakr/whatsapp 0.1.0

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 (159) hide show
  1. package/action-runtime-api.ts +1 -0
  2. package/action-runtime.runtime.ts +1 -0
  3. package/api.ts +67 -0
  4. package/auth-presence.ts +80 -0
  5. package/autobot.plugin.json +23 -0
  6. package/channel-config-api.ts +1 -0
  7. package/channel-plugin-api.ts +3 -0
  8. package/config-api.ts +4 -0
  9. package/constants.ts +1 -0
  10. package/contract-api.ts +29 -0
  11. package/directory-contract-api.ts +4 -0
  12. package/doctor-contract-api.ts +8 -0
  13. package/index.ts +16 -0
  14. package/legacy-session-surface-api.ts +6 -0
  15. package/legacy-state-migrations-api.ts +1 -0
  16. package/light-runtime-api.ts +12 -0
  17. package/login-qr-api.ts +1 -0
  18. package/login-qr-runtime.ts +23 -0
  19. package/outbound-payload-test-api.ts +1 -0
  20. package/package.json +76 -0
  21. package/runtime-api.ts +84 -0
  22. package/secret-contract-api.ts +4 -0
  23. package/security-contract-api.ts +4 -0
  24. package/setup-entry.ts +21 -0
  25. package/setup-plugin-api.ts +3 -0
  26. package/src/account-config.ts +77 -0
  27. package/src/account-ids.ts +17 -0
  28. package/src/account-types.ts +5 -0
  29. package/src/accounts.ts +176 -0
  30. package/src/action-runtime-target-auth.ts +27 -0
  31. package/src/action-runtime.ts +76 -0
  32. package/src/active-listener.ts +17 -0
  33. package/src/agent-tools-login.ts +113 -0
  34. package/src/approval-auth.ts +27 -0
  35. package/src/auth-store.runtime.ts +1 -0
  36. package/src/auth-store.ts +494 -0
  37. package/src/auto-reply/config.runtime.ts +16 -0
  38. package/src/auto-reply/constants.ts +1 -0
  39. package/src/auto-reply/deliver-reply.ts +332 -0
  40. package/src/auto-reply/loggers.ts +6 -0
  41. package/src/auto-reply/mentions.ts +131 -0
  42. package/src/auto-reply/monitor/ack-reaction.ts +99 -0
  43. package/src/auto-reply/monitor/audio-preflight.runtime.ts +9 -0
  44. package/src/auto-reply/monitor/broadcast.ts +153 -0
  45. package/src/auto-reply/monitor/commands.ts +19 -0
  46. package/src/auto-reply/monitor/echo.ts +64 -0
  47. package/src/auto-reply/monitor/group-activation.runtime.ts +1 -0
  48. package/src/auto-reply/monitor/group-activation.ts +73 -0
  49. package/src/auto-reply/monitor/group-gating.runtime.ts +8 -0
  50. package/src/auto-reply/monitor/group-gating.ts +218 -0
  51. package/src/auto-reply/monitor/group-members.ts +65 -0
  52. package/src/auto-reply/monitor/inbound-context.ts +92 -0
  53. package/src/auto-reply/monitor/inbound-dispatch.runtime.ts +22 -0
  54. package/src/auto-reply/monitor/inbound-dispatch.ts +749 -0
  55. package/src/auto-reply/monitor/last-route.ts +61 -0
  56. package/src/auto-reply/monitor/listener-log.ts +28 -0
  57. package/src/auto-reply/monitor/message-line.runtime.ts +38 -0
  58. package/src/auto-reply/monitor/message-line.ts +54 -0
  59. package/src/auto-reply/monitor/on-message.ts +333 -0
  60. package/src/auto-reply/monitor/peer.ts +17 -0
  61. package/src/auto-reply/monitor/process-message.ts +584 -0
  62. package/src/auto-reply/monitor/runtime-api.ts +36 -0
  63. package/src/auto-reply/monitor/status-reaction.ts +108 -0
  64. package/src/auto-reply/monitor-state.ts +114 -0
  65. package/src/auto-reply/monitor.ts +720 -0
  66. package/src/auto-reply/reply-resolver.runtime.ts +1 -0
  67. package/src/auto-reply/types.ts +48 -0
  68. package/src/auto-reply/util.ts +62 -0
  69. package/src/auto-reply.impl.ts +6 -0
  70. package/src/auto-reply.ts +1 -0
  71. package/src/channel-actions.runtime.ts +7 -0
  72. package/src/channel-actions.ts +85 -0
  73. package/src/channel-outbound.ts +87 -0
  74. package/src/channel-react-action.runtime.ts +10 -0
  75. package/src/channel-react-action.ts +247 -0
  76. package/src/channel.runtime.ts +117 -0
  77. package/src/channel.setup.ts +32 -0
  78. package/src/channel.ts +356 -0
  79. package/src/command-policy.ts +7 -0
  80. package/src/config-accessors.ts +22 -0
  81. package/src/config-schema.ts +6 -0
  82. package/src/config-ui-hints.ts +24 -0
  83. package/src/connection-controller-registry.ts +49 -0
  84. package/src/connection-controller.ts +680 -0
  85. package/src/creds-files.ts +19 -0
  86. package/src/creds-persistence.ts +71 -0
  87. package/src/directory-config.ts +40 -0
  88. package/src/doctor-contract.ts +11 -0
  89. package/src/doctor.ts +56 -0
  90. package/src/document-filename.ts +17 -0
  91. package/src/group-intro.ts +15 -0
  92. package/src/group-policy.ts +40 -0
  93. package/src/group-session-contract.ts +20 -0
  94. package/src/group-session-key.ts +42 -0
  95. package/src/heartbeat.ts +34 -0
  96. package/src/identity.ts +164 -0
  97. package/src/inbound/access-control.ts +187 -0
  98. package/src/inbound/dedupe.ts +132 -0
  99. package/src/inbound/extract.ts +484 -0
  100. package/src/inbound/lifecycle.ts +39 -0
  101. package/src/inbound/media.ts +128 -0
  102. package/src/inbound/monitor.ts +1042 -0
  103. package/src/inbound/outbound-mentions.ts +260 -0
  104. package/src/inbound/runtime-api.ts +7 -0
  105. package/src/inbound/save-media.runtime.ts +1 -0
  106. package/src/inbound/send-api.ts +203 -0
  107. package/src/inbound/send-result.ts +109 -0
  108. package/src/inbound/types.ts +107 -0
  109. package/src/inbound-policy.ts +215 -0
  110. package/src/inbound.ts +9 -0
  111. package/src/login-qr.ts +542 -0
  112. package/src/login.ts +83 -0
  113. package/src/media.ts +10 -0
  114. package/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test-support.ts +417 -0
  115. package/src/monitor-inbox.append-upsert.test-support.ts +133 -0
  116. package/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test-support.ts +418 -0
  117. package/src/monitor-inbox.captures-media-path-image-messages.test-support.ts +308 -0
  118. package/src/monitor-inbox.streams-inbound-messages.test-support.ts +824 -0
  119. package/src/normalize-target.ts +148 -0
  120. package/src/normalize.ts +8 -0
  121. package/src/outbound-adapter.ts +36 -0
  122. package/src/outbound-base.ts +256 -0
  123. package/src/outbound-media-contract.ts +307 -0
  124. package/src/outbound-media.runtime.ts +41 -0
  125. package/src/outbound-send-deps.ts +1 -0
  126. package/src/outbound-test-support.ts +16 -0
  127. package/src/qa-driver.runtime.ts +189 -0
  128. package/src/qr-image.ts +1 -0
  129. package/src/qr-terminal.ts +1 -0
  130. package/src/quoted-message.ts +184 -0
  131. package/src/reaction-level.ts +24 -0
  132. package/src/reconnect.ts +55 -0
  133. package/src/resolve-outbound-target.ts +58 -0
  134. package/src/runtime-api.ts +59 -0
  135. package/src/runtime-group-policy.ts +16 -0
  136. package/src/runtime.ts +9 -0
  137. package/src/security-contract.ts +47 -0
  138. package/src/security-fix.ts +71 -0
  139. package/src/send.ts +342 -0
  140. package/src/session-contract.ts +43 -0
  141. package/src/session-errors.ts +125 -0
  142. package/src/session-route.ts +32 -0
  143. package/src/session.runtime.ts +8 -0
  144. package/src/session.ts +327 -0
  145. package/src/setup-core.ts +52 -0
  146. package/src/setup-finalize.ts +450 -0
  147. package/src/setup-surface.ts +71 -0
  148. package/src/setup-test-helpers.ts +217 -0
  149. package/src/shared.ts +291 -0
  150. package/src/socket-timing.ts +38 -0
  151. package/src/state-migrations.ts +55 -0
  152. package/src/status-issues.ts +185 -0
  153. package/src/system-prompt.ts +31 -0
  154. package/src/targets-runtime.ts +221 -0
  155. package/src/text-runtime.ts +18 -0
  156. package/src/vcard.ts +84 -0
  157. package/targets.ts +5 -0
  158. package/test-api.ts +2 -0
  159. package/tsconfig.json +16 -0
@@ -0,0 +1,584 @@
1
+ import {
2
+ logAckFailure,
3
+ removeAckReactionHandleAfterReply,
4
+ type AckReactionHandle,
5
+ } from "autobot/plugin-sdk/channel-feedback";
6
+ import type { CommandTurnContext } from "autobot/plugin-sdk/channel-inbound";
7
+ import { recordInboundSession } from "autobot/plugin-sdk/conversation-runtime";
8
+ import {
9
+ createInternalHookEvent,
10
+ deriveInboundMessageHookContext,
11
+ fireAndForgetBoundedHook,
12
+ toInternalMessageReceivedContext,
13
+ toPluginMessageContext,
14
+ toPluginMessageReceivedEvent,
15
+ triggerInternalHook,
16
+ } from "autobot/plugin-sdk/hook-runtime";
17
+ import { runInboundReplyTurn } from "autobot/plugin-sdk/inbound-reply-dispatch";
18
+ import { getGlobalHookRunner } from "autobot/plugin-sdk/plugin-runtime";
19
+ import { resolveBatchedReplyThreadingPolicy } from "autobot/plugin-sdk/reply-reference";
20
+ import { getPrimaryIdentityId, getSelfIdentity, getSenderIdentity } from "../../identity.js";
21
+ import {
22
+ resolveWhatsAppCommandAuthorized,
23
+ resolveWhatsAppInboundPolicy,
24
+ } from "../../inbound-policy.js";
25
+ import { newConnectionId } from "../../reconnect.js";
26
+ import { formatError } from "../../session.js";
27
+ import {
28
+ resolveWhatsAppDirectSystemPrompt,
29
+ resolveWhatsAppGroupSystemPrompt,
30
+ } from "../../system-prompt.js";
31
+ import { deliverWebReply } from "../deliver-reply.js";
32
+ import { whatsappInboundLog } from "../loggers.js";
33
+ import type { WebInboundMsg } from "../types.js";
34
+ import { elide } from "../util.js";
35
+ import { maybeSendAckReaction } from "./ack-reaction.js";
36
+ import {
37
+ resolveVisibleWhatsAppGroupHistory,
38
+ resolveVisibleWhatsAppReplyContext,
39
+ type GroupHistoryEntry,
40
+ } from "./inbound-context.js";
41
+ import {
42
+ buildWhatsAppInboundContext,
43
+ dispatchWhatsAppBufferedReply,
44
+ resolveWhatsAppDmRouteTarget,
45
+ resolveWhatsAppResponsePrefix,
46
+ updateWhatsAppMainLastRoute,
47
+ } from "./inbound-dispatch.js";
48
+ import { trackBackgroundTask, updateLastRouteInBackground } from "./last-route.js";
49
+ import { buildInboundLine } from "./message-line.js";
50
+ import {
51
+ buildHistoryContextFromEntries,
52
+ createChannelMessageReplyPipeline,
53
+ formatInboundEnvelope,
54
+ logVerbose,
55
+ normalizeE164,
56
+ resolveChannelContextVisibilityMode,
57
+ resolveInboundSessionEnvelopeContext,
58
+ resolvePinnedMainDmOwnerFromAllowlist,
59
+ isControlCommandMessage,
60
+ shouldComputeCommandAuthorized,
61
+ shouldLogVerbose,
62
+ type getChildLogger,
63
+ type getReplyFromConfig,
64
+ type HistoryEntry,
65
+ type LoadConfigFn,
66
+ type resolveAgentRoute,
67
+ } from "./runtime-api.js";
68
+ import {
69
+ createWhatsAppStatusReactionController,
70
+ type StatusReactionController,
71
+ } from "./status-reaction.js";
72
+
73
+ const WHATSAPP_MESSAGE_RECEIVED_HOOK_LIMITS = {
74
+ maxConcurrency: 8,
75
+ maxQueue: 128,
76
+ timeoutMs: 2_000,
77
+ };
78
+
79
+ type WhatsAppMessageReceivedHookConfig = {
80
+ pluginHooks?: {
81
+ messageReceived?: unknown;
82
+ };
83
+ accounts?: Record<string, unknown>;
84
+ };
85
+
86
+ function readWhatsAppMessageReceivedHookOptIn(value: unknown): boolean | undefined {
87
+ if (!value || typeof value !== "object") {
88
+ return undefined;
89
+ }
90
+ const pluginHooks = (value as WhatsAppMessageReceivedHookConfig).pluginHooks;
91
+ return pluginHooks?.messageReceived === true ? true : undefined;
92
+ }
93
+
94
+ function shouldEmitWhatsAppMessageReceivedHooks(params: {
95
+ cfg: ReturnType<LoadConfigFn>;
96
+ accountId?: string;
97
+ }): boolean {
98
+ const channelConfig = params.cfg.channels?.whatsapp as
99
+ | WhatsAppMessageReceivedHookConfig
100
+ | undefined;
101
+ const accountConfig =
102
+ params.accountId && channelConfig?.accounts
103
+ ? channelConfig.accounts[params.accountId]
104
+ : undefined;
105
+ return (
106
+ readWhatsAppMessageReceivedHookOptIn(accountConfig) ??
107
+ readWhatsAppMessageReceivedHookOptIn(channelConfig) ??
108
+ false
109
+ );
110
+ }
111
+
112
+ function emitWhatsAppMessageReceivedHooks(params: {
113
+ ctx: ReturnType<typeof buildWhatsAppInboundContext>;
114
+ sessionKey: string;
115
+ }): void {
116
+ const canonical = deriveInboundMessageHookContext(params.ctx);
117
+ const hookRunner = getGlobalHookRunner();
118
+ if (hookRunner?.hasHooks("message_received")) {
119
+ fireAndForgetBoundedHook(
120
+ () =>
121
+ hookRunner.runMessageReceived(
122
+ toPluginMessageReceivedEvent(canonical),
123
+ toPluginMessageContext(canonical),
124
+ ),
125
+ "whatsapp: message_received plugin hook failed",
126
+ undefined,
127
+ WHATSAPP_MESSAGE_RECEIVED_HOOK_LIMITS,
128
+ );
129
+ }
130
+ fireAndForgetBoundedHook(
131
+ () =>
132
+ triggerInternalHook(
133
+ createInternalHookEvent(
134
+ "message",
135
+ "received",
136
+ params.sessionKey,
137
+ toInternalMessageReceivedContext(canonical),
138
+ ),
139
+ ),
140
+ "whatsapp: message_received internal hook failed",
141
+ undefined,
142
+ WHATSAPP_MESSAGE_RECEIVED_HOOK_LIMITS,
143
+ );
144
+ }
145
+
146
+ function emitWhatsAppMessageReceivedHooksIfEnabled(params: {
147
+ cfg: ReturnType<LoadConfigFn>;
148
+ ctx: ReturnType<typeof buildWhatsAppInboundContext>;
149
+ accountId?: string;
150
+ sessionKey: string;
151
+ }): void {
152
+ if (
153
+ !shouldEmitWhatsAppMessageReceivedHooks({
154
+ cfg: params.cfg,
155
+ accountId: params.accountId,
156
+ })
157
+ ) {
158
+ return;
159
+ }
160
+
161
+ emitWhatsAppMessageReceivedHooks({
162
+ ctx: params.ctx,
163
+ sessionKey: params.sessionKey,
164
+ });
165
+ }
166
+
167
+ function resolvePinnedMainDmRecipient(params: {
168
+ cfg: ReturnType<LoadConfigFn>;
169
+ allowFrom?: string[];
170
+ }): string | null {
171
+ return resolvePinnedMainDmOwnerFromAllowlist({
172
+ dmScope: params.cfg.session?.dmScope,
173
+ allowFrom: params.allowFrom,
174
+ normalizeEntry: (entry) => normalizeE164(entry),
175
+ });
176
+ }
177
+
178
+ export async function processMessage(params: {
179
+ cfg: ReturnType<LoadConfigFn>;
180
+ msg: WebInboundMsg;
181
+ route: ReturnType<typeof resolveAgentRoute>;
182
+ groupHistoryKey: string;
183
+ groupHistories: Map<string, GroupHistoryEntry[]>;
184
+ groupMemberNames: Map<string, Map<string, string>>;
185
+ connectionId: string;
186
+ verbose: boolean;
187
+ maxMediaBytes: number;
188
+ replyResolver: typeof getReplyFromConfig;
189
+ replyLogger: ReturnType<typeof getChildLogger>;
190
+ backgroundTasks: Set<Promise<unknown>>;
191
+ rememberSentText: (
192
+ text: string | undefined,
193
+ opts: {
194
+ combinedBody?: string;
195
+ combinedBodySessionKey?: string;
196
+ logVerboseMessage?: boolean;
197
+ },
198
+ ) => void;
199
+ echoHas: (key: string) => boolean;
200
+ echoForget: (key: string) => void;
201
+ buildCombinedEchoKey: (p: { sessionKey: string; combinedBody: string }) => string;
202
+ maxMediaTextChunkLimit?: number;
203
+ groupHistory?: GroupHistoryEntry[];
204
+ suppressGroupHistoryClear?: boolean;
205
+ ackAlreadySent?: boolean;
206
+ ackReaction?: AckReactionHandle | null;
207
+ statusReactionController?: StatusReactionController | null;
208
+ /** Pre-computed audio transcript from a caller-level preflight, used to avoid
209
+ * re-transcribing the same voice note once per broadcast agent.
210
+ * - string → transcript obtained; use it directly, skip internal STT
211
+ * - null → preflight was attempted but failed / returned nothing; skip internal STT
212
+ * - undefined (omitted) → caller did not attempt preflight; run internal STT as normal */
213
+ preflightAudioTranscript?: string | null;
214
+ }) {
215
+ const conversationId = params.msg.conversationId ?? params.msg.from;
216
+ const self = getSelfIdentity(params.msg);
217
+ const inboundPolicy = resolveWhatsAppInboundPolicy({
218
+ cfg: params.cfg,
219
+ accountId: params.route.accountId ?? params.msg.accountId,
220
+ selfE164: self.e164 ?? null,
221
+ });
222
+ const account = inboundPolicy.account;
223
+ const contextVisibilityMode = resolveChannelContextVisibilityMode({
224
+ cfg: params.cfg,
225
+ channel: "whatsapp",
226
+ accountId: account.accountId,
227
+ });
228
+ const { storePath, envelopeOptions, previousTimestamp } = resolveInboundSessionEnvelopeContext({
229
+ cfg: params.cfg,
230
+ agentId: params.route.agentId,
231
+ sessionKey: params.route.sessionKey,
232
+ });
233
+ // Preflight audio transcription: transcribe voice notes before building the
234
+ // inbound context so the agent receives the transcript instead of <media:audio>.
235
+ // Mirrors the preflight step added for Telegram in #61008.
236
+ // When the caller already performed transcription (e.g. on-message.ts before
237
+ // broadcast fan-out) the pre-computed result is reused to avoid N STT calls
238
+ // for N broadcast agents on the same voice note.
239
+ // preflightAudioTranscript semantics:
240
+ // string → transcript ready, use it
241
+ // null → caller attempted but got nothing; skip internal STT to avoid retry
242
+ // undefined → caller did not attempt; run internal STT
243
+ let audioTranscript: string | undefined = params.preflightAudioTranscript ?? undefined;
244
+ const hasAudioBody =
245
+ params.msg.mediaType?.startsWith("audio/") === true && params.msg.body === "<media:audio>";
246
+ if (params.preflightAudioTranscript === undefined && hasAudioBody && params.msg.mediaPath) {
247
+ try {
248
+ const { transcribeFirstAudio } = await import("./audio-preflight.runtime.js");
249
+ audioTranscript = await transcribeFirstAudio({
250
+ ctx: {
251
+ MediaPaths: [params.msg.mediaPath],
252
+ MediaTypes: params.msg.mediaType ? [params.msg.mediaType] : undefined,
253
+ From: params.msg.from,
254
+ To: params.msg.to,
255
+ Provider: "whatsapp",
256
+ Surface: "whatsapp",
257
+ OriginatingChannel: "whatsapp",
258
+ OriginatingTo: conversationId,
259
+ AccountId: params.route.accountId,
260
+ },
261
+ cfg: params.cfg,
262
+ });
263
+ } catch {
264
+ // Transcription failure is non-fatal: fall back to <media:audio> placeholder.
265
+ if (shouldLogVerbose()) {
266
+ logVerbose("whatsapp: audio preflight transcription failed, using placeholder");
267
+ }
268
+ }
269
+ }
270
+
271
+ // If we have a transcript, replace the agent-facing body so the agent sees the spoken text.
272
+ // mediaPath and mediaType are intentionally preserved so that inboundAudio detection
273
+ // (used by features such as messages.tts.auto: "inbound") still sees this as an
274
+ // audio message. The transcript and transcribed media index are also stored on
275
+ // context so downstream media understanding does not transcribe it again.
276
+ const msgForAgent =
277
+ audioTranscript !== undefined ? { ...params.msg, body: audioTranscript } : params.msg;
278
+
279
+ let combinedBody = buildInboundLine({
280
+ cfg: params.cfg,
281
+ msg: msgForAgent,
282
+ agentId: params.route.agentId,
283
+ previousTimestamp,
284
+ envelope: envelopeOptions,
285
+ });
286
+ let shouldClearGroupHistory = false;
287
+ const visibleGroupHistory =
288
+ params.msg.chatType === "group"
289
+ ? resolveVisibleWhatsAppGroupHistory({
290
+ history: params.groupHistory ?? params.groupHistories.get(params.groupHistoryKey) ?? [],
291
+ mode: contextVisibilityMode,
292
+ groupPolicy: inboundPolicy.groupPolicy,
293
+ groupAllowFrom: inboundPolicy.groupAllowFrom,
294
+ })
295
+ : undefined;
296
+
297
+ if (params.msg.chatType === "group") {
298
+ const history = visibleGroupHistory ?? [];
299
+ if (history.length > 0) {
300
+ const historyEntries: HistoryEntry[] = history.map((m) => ({
301
+ sender: m.sender,
302
+ body: m.body,
303
+ timestamp: m.timestamp,
304
+ }));
305
+ combinedBody = buildHistoryContextFromEntries({
306
+ entries: historyEntries,
307
+ currentMessage: combinedBody,
308
+ excludeLast: false,
309
+ formatEntry: (entry) => {
310
+ return formatInboundEnvelope({
311
+ channel: "WhatsApp",
312
+ from: conversationId,
313
+ timestamp: entry.timestamp,
314
+ body: entry.body,
315
+ chatType: "group",
316
+ senderLabel: entry.sender,
317
+ envelope: envelopeOptions,
318
+ });
319
+ },
320
+ });
321
+ }
322
+ shouldClearGroupHistory = !(params.suppressGroupHistoryClear ?? false);
323
+ }
324
+
325
+ // Echo detection uses combined body so we don't respond twice.
326
+ const combinedEchoKey = params.buildCombinedEchoKey({
327
+ sessionKey: params.route.sessionKey,
328
+ combinedBody,
329
+ });
330
+ if (params.echoHas(combinedEchoKey)) {
331
+ logVerbose("Skipping auto-reply: detected echo for combined message");
332
+ params.echoForget(combinedEchoKey);
333
+ return false;
334
+ }
335
+
336
+ // When statusReactions.enabled, a StatusReactionController takes over lifecycle
337
+ // signaling (queued → thinking → tool → done/error). The plain ackReaction is
338
+ // skipped so the same message slot isn't used for two competing systems.
339
+ const statusReactionController =
340
+ params.statusReactionController ??
341
+ (params.cfg.messages?.statusReactions?.enabled === true && !params.ackAlreadySent
342
+ ? await createWhatsAppStatusReactionController({
343
+ cfg: params.cfg,
344
+ msg: params.msg,
345
+ agentId: params.route.agentId,
346
+ sessionKey: params.route.sessionKey,
347
+ conversationId,
348
+ verbose: params.verbose,
349
+ accountId: account.accountId,
350
+ })
351
+ : null);
352
+
353
+ if (statusReactionController && !params.statusReactionController) {
354
+ void statusReactionController.setQueued();
355
+ }
356
+
357
+ // Send ack reaction immediately upon message receipt (post-gating). Callers
358
+ // that do preflight work before processMessage can send it first and set
359
+ // ackAlreadySent so slow STT does not delay user-visible receipt feedback.
360
+ // Skip if the status reaction controller is handling lifecycle signaling.
361
+ let ackReaction = params.ackReaction ?? null;
362
+ if (!statusReactionController && !ackReaction && params.ackAlreadySent !== true) {
363
+ ackReaction = await maybeSendAckReaction({
364
+ cfg: params.cfg,
365
+ msg: params.msg,
366
+ agentId: params.route.agentId,
367
+ sessionKey: params.route.sessionKey,
368
+ conversationId,
369
+ verbose: params.verbose,
370
+ accountId: account.accountId,
371
+ info: params.replyLogger.info.bind(params.replyLogger),
372
+ warn: params.replyLogger.warn.bind(params.replyLogger),
373
+ });
374
+ }
375
+
376
+ const correlationId = params.msg.id ?? newConnectionId();
377
+ params.replyLogger.info(
378
+ {
379
+ connectionId: params.connectionId,
380
+ correlationId,
381
+ from: params.msg.chatType === "group" ? conversationId : params.msg.from,
382
+ to: params.msg.to,
383
+ body: elide(combinedBody, 240),
384
+ mediaType: params.msg.mediaType ?? null,
385
+ mediaPath: params.msg.mediaPath ?? null,
386
+ },
387
+ "inbound web message",
388
+ );
389
+
390
+ const fromDisplay = params.msg.chatType === "group" ? conversationId : params.msg.from;
391
+ const kindLabel = params.msg.mediaType ? `, ${params.msg.mediaType}` : "";
392
+ whatsappInboundLog.info(
393
+ `Inbound message ${fromDisplay} -> ${params.msg.to} (${params.msg.chatType}${kindLabel}, ${combinedBody.length} chars)`,
394
+ );
395
+ if (shouldLogVerbose()) {
396
+ whatsappInboundLog.debug(`Inbound body: ${elide(combinedBody, 400)}`);
397
+ }
398
+
399
+ const sender = getSenderIdentity(params.msg);
400
+ const visibleReplyTo = resolveVisibleWhatsAppReplyContext({
401
+ msg: params.msg,
402
+ authDir: account.authDir,
403
+ mode: contextVisibilityMode,
404
+ groupPolicy: inboundPolicy.groupPolicy,
405
+ groupAllowFrom: inboundPolicy.groupAllowFrom,
406
+ });
407
+ const dmRouteTarget = resolveWhatsAppDmRouteTarget({
408
+ msg: params.msg,
409
+ senderE164: sender.e164 ?? undefined,
410
+ normalizeE164,
411
+ });
412
+ const shouldCheckCommandAuth = shouldComputeCommandAuthorized(params.msg.body, params.cfg);
413
+ const isTextCommand = isControlCommandMessage(params.msg.body, params.cfg);
414
+ const commandAuthorized = shouldCheckCommandAuth
415
+ ? await resolveWhatsAppCommandAuthorized({
416
+ cfg: params.cfg,
417
+ msg: params.msg,
418
+ policy: inboundPolicy,
419
+ })
420
+ : undefined;
421
+ const commandTurn: CommandTurnContext = isTextCommand
422
+ ? {
423
+ kind: "text-slash",
424
+ source: "text",
425
+ authorized: Boolean(commandAuthorized),
426
+ body: params.msg.body,
427
+ }
428
+ : {
429
+ kind: "normal",
430
+ source: "message",
431
+ authorized: false,
432
+ body: params.msg.body,
433
+ };
434
+ const { onModelSelected, ...replyPipeline } = createChannelMessageReplyPipeline({
435
+ cfg: params.cfg,
436
+ agentId: params.route.agentId,
437
+ channel: "whatsapp",
438
+ accountId: params.route.accountId,
439
+ });
440
+ const responsePrefix = resolveWhatsAppResponsePrefix({
441
+ cfg: params.cfg,
442
+ agentId: params.route.agentId,
443
+ isSelfChat: params.msg.chatType !== "group" && inboundPolicy.isSelfChat,
444
+ pipelineResponsePrefix: replyPipeline.responsePrefix,
445
+ });
446
+ const replyThreading = resolveBatchedReplyThreadingPolicy(
447
+ account.replyToMode ?? "off",
448
+ params.msg.isBatched === true,
449
+ );
450
+
451
+ // Resolve combined conversation system prompt using the group or direct surface.
452
+ const conversationSystemPrompt =
453
+ params.msg.chatType === "group"
454
+ ? resolveWhatsAppGroupSystemPrompt({
455
+ accountConfig: account,
456
+ groupId: conversationId,
457
+ })
458
+ : resolveWhatsAppDirectSystemPrompt({
459
+ accountConfig: account,
460
+ peerId: dmRouteTarget ?? params.msg.from,
461
+ });
462
+
463
+ const ctxPayload = buildWhatsAppInboundContext({
464
+ bodyForAgent: msgForAgent.body,
465
+ combinedBody,
466
+ commandBody: params.msg.body,
467
+ commandAuthorized,
468
+ commandTurn,
469
+ conversationId,
470
+ groupHistory: visibleGroupHistory,
471
+ groupMemberRoster: params.groupMemberNames.get(params.groupHistoryKey),
472
+ groupSystemPrompt: conversationSystemPrompt,
473
+ msg: params.msg,
474
+ rawBody: params.msg.body,
475
+ route: params.route,
476
+ sender: {
477
+ id: getPrimaryIdentityId(sender) ?? undefined,
478
+ name: sender.name ?? undefined,
479
+ e164: sender.e164 ?? undefined,
480
+ },
481
+ ...(audioTranscript !== undefined ? { transcript: audioTranscript } : {}),
482
+ ...(audioTranscript !== undefined ? { mediaTranscribedIndexes: [0] } : {}),
483
+ replyThreading,
484
+ visibleReplyTo: visibleReplyTo ?? undefined,
485
+ });
486
+ emitWhatsAppMessageReceivedHooksIfEnabled({
487
+ cfg: params.cfg,
488
+ ctx: ctxPayload,
489
+ accountId: params.route.accountId,
490
+ sessionKey: params.route.sessionKey,
491
+ });
492
+
493
+ const pinnedMainDmRecipient = resolvePinnedMainDmRecipient({
494
+ cfg: params.cfg,
495
+ allowFrom: inboundPolicy.configuredAllowFrom,
496
+ });
497
+ updateWhatsAppMainLastRoute({
498
+ backgroundTasks: params.backgroundTasks,
499
+ cfg: params.cfg,
500
+ ctx: ctxPayload,
501
+ dmRouteTarget,
502
+ pinnedMainDmRecipient,
503
+ route: params.route,
504
+ updateLastRoute: updateLastRouteInBackground,
505
+ warn: params.replyLogger.warn.bind(params.replyLogger),
506
+ });
507
+
508
+ const turnResult = await runInboundReplyTurn({
509
+ channel: "whatsapp",
510
+ accountId: params.route.accountId,
511
+ raw: params.msg,
512
+ adapter: {
513
+ ingest: () => ({
514
+ id: params.msg.id ?? `${conversationId}:${Date.now()}`,
515
+ timestamp: params.msg.timestamp,
516
+ rawText: ctxPayload.RawBody ?? "",
517
+ textForAgent: ctxPayload.BodyForAgent,
518
+ textForCommands: ctxPayload.CommandBody,
519
+ raw: params.msg,
520
+ }),
521
+ resolveTurn: () => ({
522
+ channel: "whatsapp",
523
+ accountId: params.route.accountId,
524
+ routeSessionKey: params.route.sessionKey,
525
+ storePath,
526
+ ctxPayload,
527
+ recordInboundSession,
528
+ record: {
529
+ onRecordError: (err) => {
530
+ params.replyLogger.warn(
531
+ {
532
+ error: formatError(err),
533
+ storePath,
534
+ sessionKey: params.route.sessionKey,
535
+ },
536
+ "failed updating session meta",
537
+ );
538
+ },
539
+ trackSessionMetaTask: (task) => {
540
+ trackBackgroundTask(params.backgroundTasks, task);
541
+ },
542
+ },
543
+ runDispatch: () =>
544
+ dispatchWhatsAppBufferedReply({
545
+ cfg: params.cfg,
546
+ connectionId: params.connectionId,
547
+ context: ctxPayload,
548
+ conversationId,
549
+ deliverReply: deliverWebReply,
550
+ groupHistories: params.groupHistories,
551
+ groupHistoryKey: params.groupHistoryKey,
552
+ maxMediaBytes: params.maxMediaBytes,
553
+ maxMediaTextChunkLimit: params.maxMediaTextChunkLimit,
554
+ msg: params.msg,
555
+ onModelSelected,
556
+ rememberSentText: params.rememberSentText,
557
+ replyLogger: params.replyLogger,
558
+ replyPipeline: {
559
+ ...replyPipeline,
560
+ responsePrefix,
561
+ },
562
+ replyResolver: params.replyResolver,
563
+ route: params.route,
564
+ shouldClearGroupHistory,
565
+ statusReactionController,
566
+ }),
567
+ }),
568
+ },
569
+ });
570
+ const didSendReply = turnResult.dispatched ? turnResult.dispatchResult : false;
571
+ removeAckReactionHandleAfterReply({
572
+ removeAfterReply: Boolean(params.cfg.messages?.removeAckAfterReply && didSendReply),
573
+ ackReaction,
574
+ onError: (err) => {
575
+ logAckFailure({
576
+ log: logVerbose,
577
+ channel: "whatsapp",
578
+ target: `${params.msg.chatId ?? conversationId}/${params.msg.id ?? "unknown"}`,
579
+ error: err,
580
+ });
581
+ },
582
+ });
583
+ return didSendReply;
584
+ }
@@ -0,0 +1,36 @@
1
+ export { resolveIdentityNamePrefix } from "autobot/plugin-sdk/agent-runtime";
2
+ export { formatInboundEnvelope } from "autobot/plugin-sdk/channel-envelope";
3
+ export { resolveInboundSessionEnvelopeContext } from "autobot/plugin-sdk/channel-inbound";
4
+ export { toLocationContext } from "autobot/plugin-sdk/channel-location";
5
+ export {
6
+ createChannelMessageReplyPipeline,
7
+ resolveChannelMessageSourceReplyDeliveryMode,
8
+ } from "autobot/plugin-sdk/channel-message";
9
+ export {
10
+ isControlCommandMessage,
11
+ shouldComputeCommandAuthorized,
12
+ } from "autobot/plugin-sdk/command-detection";
13
+ export { resolveChannelContextVisibilityMode } from "../config.runtime.js";
14
+ export { getAgentScopedMediaLocalRoots } from "autobot/plugin-sdk/media-runtime";
15
+ export type LoadConfigFn = typeof import("../config.runtime.js").getRuntimeConfig;
16
+ export {
17
+ buildHistoryContextFromEntries,
18
+ type HistoryEntry,
19
+ } from "autobot/plugin-sdk/reply-history";
20
+ export { resolveSendableOutboundReplyParts } from "autobot/plugin-sdk/reply-payload";
21
+ export {
22
+ dispatchReplyWithBufferedBlockDispatcher,
23
+ finalizeInboundContext,
24
+ resolveChunkMode,
25
+ resolveTextChunkLimit,
26
+ type getReplyFromConfig,
27
+ type ReplyPayload,
28
+ } from "autobot/plugin-sdk/reply-runtime";
29
+ export {
30
+ resolveInboundLastRouteSessionKey,
31
+ type resolveAgentRoute,
32
+ } from "autobot/plugin-sdk/routing";
33
+ export { logVerbose, shouldLogVerbose, type getChildLogger } from "autobot/plugin-sdk/runtime-env";
34
+ export { resolvePinnedMainDmOwnerFromAllowlist } from "autobot/plugin-sdk/security-runtime";
35
+ export { resolveMarkdownTableMode } from "autobot/plugin-sdk/markdown-table-runtime";
36
+ export { jidToE164, normalizeE164 } from "../../text-runtime.js";