@openclaw/whatsapp 2026.7.2-beta.7 → 2026.8.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/{accounts-CSqLvCY5.js → accounts-BX2fZUyR.js} +1 -1
  2. package/dist/{action-runtime-BzfDFPbH.js → action-runtime-B4e3XFOY.js} +2 -2
  3. package/dist/action-runtime-api.js +1 -1
  4. package/dist/action-runtime.runtime.js +1 -1
  5. package/dist/{agent-tools-login-DE9MU98T.js → agent-tools-login-BjRpD-JQ.js} +5 -4
  6. package/dist/api.js +69 -127
  7. package/dist/{approval-handler.runtime-DrA59QSA.js → approval-handler.runtime-fgGZhALv.js} +4 -4
  8. package/dist/{approval-reactions-Cu66aYdq.js → approval-reactions-CmtJCxe_.js} +25 -73
  9. package/dist/auth-presence.js +1 -1
  10. package/dist/{auth-store-DcEpIOmE.js → auth-store-B_DQIdzp.js} +79 -12
  11. package/dist/call-tool-api.js +1 -1
  12. package/dist/{channel-Dg62olhy.js → channel-DJVQHz_V.js} +56 -160
  13. package/dist/channel-plugin-api.js +1 -1
  14. package/dist/{channel-react-action-J_YtyMGX.js → channel-react-action-d6OacTNj.js} +3 -3
  15. package/dist/{channel.runtime-BP0xNmLi.js → channel.runtime-RxuNGZ7w.js} +4 -4
  16. package/dist/channel.setup-Cmvd967u.js +14 -0
  17. package/dist/{connection-controller-BOB2MVKY.js → connection-controller-CUyZgGpf.js} +2 -3
  18. package/dist/contract-api.js +2 -3
  19. package/dist/{creds-files-B1kSWtBg.js → creds-files-W6B3EMZ8.js} +17 -1
  20. package/dist/{directory-config-DalGrfGN.js → directory-config-Pc4f07wB.js} +3 -3
  21. package/dist/directory-contract-api.js +1 -1
  22. package/dist/doctor-contract-api.js +37 -2
  23. package/dist/{group-session-key-DVMZlO3x.js → group-session-key-DmFc_Cqi.js} +23 -4
  24. package/dist/light-runtime-api.js +2 -2
  25. package/dist/{login-CvY2Dhch.js → login-D6_rfxTZ.js} +4 -4
  26. package/dist/{login-qr-Bncpb6js.js → login-qr-B0VyVSqQ.js} +4 -4
  27. package/dist/login-qr-runtime.js +1 -1
  28. package/dist/{monitor-CfkaUSL2.js → monitor-BCe5Mm-1.js} +574 -163
  29. package/dist/{question-reactions-CRsnyoMK.js → question-reactions-D3eAeB0i.js} +29 -78
  30. package/dist/runtime-api.js +9 -9
  31. package/dist/runtime-group-policy-DeSRFXEV.js +11 -0
  32. package/dist/{send-api-7wk6eQyW.js → send-api-DimZST3E.js} +86 -107
  33. package/dist/{send-D37ddk22.js → send-prK8F7Vj.js} +277 -16
  34. package/dist/setup-entry.js +1 -8
  35. package/dist/{setup-finalize-BZucx24W.js → setup-finalize-D3n71Uyx.js} +4 -4
  36. package/dist/setup-plugin-api.js +1 -1
  37. package/dist/{setup-surface-BkVcd769.js → setup-surface-BCZszf3l.js} +3 -3
  38. package/dist/{shared-CAxd15bp.js → shared-Dp9M3ZPX.js} +5 -6
  39. package/dist/{socket-close-DridEuzz.js → socket-close-09FojDO2.js} +4 -10
  40. package/openclaw.plugin.json +4 -0
  41. package/package.json +4 -5
  42. package/skills/wacli/SKILL.md +8 -1
  43. package/dist/access-control-pZcxTmd5.js +0 -363
  44. package/dist/approval-resolver-DoeBBt22.js +0 -16
  45. package/dist/channel.setup-pMwNVkCA.js +0 -19
  46. package/dist/identity-CPrT5aEU.js +0 -76
  47. package/dist/legacy-state-migrations-api.js +0 -2
  48. package/dist/state-migrations-CAMTrs2U.js +0 -33
@@ -1,53 +1,58 @@
1
- import { a as resolveWhatsAppAccount, s as resolveWhatsAppMediaMaxBytes } from "./accounts-CSqLvCY5.js";
2
- import { a as normalizeWhatsAppOutboundPayload, c as prepareWhatsAppOutboundMedia, r as sendReactionWhatsApp, s as normalizeWhatsAppPayloadTextPreservingIndentation, u as resolveWhatsAppReactionLevel } from "./send-D37ddk22.js";
1
+ import { a as resolveWhatsAppAccount, s as resolveWhatsAppMediaMaxBytes } from "./accounts-BX2fZUyR.js";
2
+ import { _ as resolveWhatsAppReactionLevel, a as normalizeWhatsAppOutboundPayload, c as prepareWhatsAppOutboundMedia, d as mergeWhatsAppAcceptedSendError, f as normalizeWhatsAppSendResult, h as withWhatsAppLogicalDeliveryActivity, m as rememberWhatsAppPartialSend, p as rememberWhatsAppAcceptedSend, r as sendReactionWhatsApp, s as normalizeWhatsAppPayloadTextPreservingIndentation, u as listWhatsAppSendResultMessageIds } from "./send-prK8F7Vj.js";
3
3
  import { i as getWhatsAppRuntime } from "./runtime-BLlToOi6.js";
4
4
  import { r as getWhatsAppConnectionController } from "./connection-controller-runtime-context-DG0tuYuw.js";
5
5
  import { a as markdownToWhatsAppChunks, c as toWhatsappJid, l as toWhatsappJidWithLid, n as isSelfChatMode, o as resolveEquivalentWhatsAppDirectChatJids, r as jidToE164, s as resolveJidToE164 } from "./targets-runtime-TMk0lGuy.js";
6
6
  import { t as normalizeE164 } from "./text-runtime-CaKLhowd.js";
7
- import { a as cacheInboundMessageMeta, i as buildQuotedMessageOptions, n as resolveWhatsAppGroupSessionRoute, o as lookupInboundMessageMeta, r as resolveWhatsAppLegacyGroupSessionKey } from "./group-session-key-DVMZlO3x.js";
8
- import { g as readWebSelfId, o as getWebAuthAgeMs, r as WhatsAppAuthUnstableError, v as readWebSelfIdentityForDecision } from "./auth-store-DcEpIOmE.js";
9
- import { a as getSelfIdentity, c as resolveComparableIdentity, i as getReplyContext, n as getMentionIdentities, o as getSenderIdentity, r as getPrimaryIdentityId, s as identitiesOverlap, t as getComparableIdentityValues } from "./identity-CPrT5aEU.js";
7
+ import { a as cacheInboundMessageMeta, i as buildQuotedMessageOptions, n as resolveWhatsAppGroupSessionRoute, o as lookupInboundMessageMeta, r as resolveWhatsAppLegacyGroupSessionKey } from "./group-session-key-DmFc_Cqi.js";
8
+ import { C as getMentionIdentities, D as getSenderIdentity, E as getSelfIdentity, O as identitiesOverlap, S as getComparableIdentityValues, T as getReplyContext, g as readWebSelfId, k as resolveComparableIdentity, o as getWebAuthAgeMs, r as WhatsAppAuthUnstableError, v as readWebSelfIdentityForDecision, w as getPrimaryIdentityId } from "./auth-store-B_DQIdzp.js";
10
9
  import { n as resolveWhatsAppGroupsConfigPath } from "./group-config-path-BGyzT9Lg.js";
11
- import { a as waitForWaConnection, c as isWhatsAppSocketOperationTimeoutError, d as withWhatsAppSocketOperationTimeout, l as resolveWhatsAppSocketOperationTimeoutMs, r as createWaSocket, s as createWhatsAppSocketOperationTimeoutAdapter, u as resolveWhatsAppSocketTiming } from "./socket-close-DridEuzz.js";
10
+ import { a as waitForWaConnection, c as isWhatsAppSocketOperationTimeoutError, d as withWhatsAppSocketOperationTimeout, l as resolveWhatsAppSocketOperationTimeoutMs, r as createWaSocket, s as createWhatsAppSocketOperationTimeoutAdapter, u as resolveWhatsAppSocketTiming } from "./socket-close-09FojDO2.js";
12
11
  import { n as getStatusCode, t as formatError } from "./session-errors-JuazczrA.js";
13
12
  import { t as BufferJSON } from "./session.runtime-Nsqaehbd.js";
14
- import { c as requireWhatsAppInboundAdmission, i as resolveWhatsAppInboundPolicy, l as resolveWhatsAppGroupConversationId, o as buildDeprecatedFlatWhatsAppInboundAdmission, r as resolveWhatsAppCommandAuthorized, s as requireAdmittedWhatsAppInboundMessage, t as checkInboundAccessControl } from "./access-control-pZcxTmd5.js";
15
- import { _ as extractMediaKind, a as mayContainWhatsAppOutboundMention, b as hasInboundUserContent, c as DisconnectReason, d as normalizeMessageContent, f as describeReplyContext, g as extractLocationData, h as extractExternalAdReplyContext, i as addWhatsAppOutboundMentionsToContent, l as downloadMediaMessage, m as extractContextInfo, n as listWhatsAppSendResultMessageIds, o as resolveWhatsAppOutboundMentions, p as extractContactContext, r as normalizeWhatsAppSendResult, s as addWhatsAppImagePreviewFields, t as createWebSendApi, u as isJidGroup, v as extractMentionedJids, x as resolveInboundMediaMimetype, y as extractText } from "./send-api-7wk6eQyW.js";
16
- import { c as computeBackoff, d as resolveReconnectPolicy, f as sleepWithAbort, l as newConnectionId, n as WHATSAPP_WATCHDOG_TIMEOUT_ERROR, r as WhatsAppConnectionController, s as DEFAULT_RECONNECT_POLICY, u as resolveHeartbeatSeconds } from "./connection-controller-BOB2MVKY.js";
17
- import { n as maybeResolveWhatsAppApprovalReaction } from "./approval-reactions-Cu66aYdq.js";
18
- import { t as maybeResolveWhatsAppQuestionReaction } from "./question-reactions-CRsnyoMK.js";
19
- import { normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
13
+ import { a as addWhatsAppImagePreviewFields, c as extractContextInfo, d as extractMediaKind, f as extractMentionedJids, g as resolveInboundMediaMimetype, h as hasInboundUserContent, i as resolveWhatsAppOutboundMentions, l as extractExternalAdReplyContext, n as addWhatsAppOutboundMentionsToContent, o as describeReplyContext, p as extractText, r as mayContainWhatsAppOutboundMention, s as extractContactContext, t as createWebSendApi, u as extractLocationData } from "./send-api-DimZST3E.js";
14
+ import { t as resolveWhatsAppRuntimeGroupPolicy } from "./runtime-group-policy-DeSRFXEV.js";
15
+ import { c as computeBackoff, d as resolveReconnectPolicy, f as sleepWithAbort, l as newConnectionId, n as WHATSAPP_WATCHDOG_TIMEOUT_ERROR, r as WhatsAppConnectionController, s as DEFAULT_RECONNECT_POLICY, u as resolveHeartbeatSeconds } from "./connection-controller-CUyZgGpf.js";
16
+ import { n as maybeResolveWhatsAppApprovalReaction } from "./approval-reactions-CmtJCxe_.js";
17
+ import { t as maybeResolveWhatsAppQuestionReaction } from "./question-reactions-D3eAeB0i.js";
18
+ import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
20
19
  import { filterSupplementalContextItems, resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
21
- import { createAckReactionHandle, createStatusReactionController, logAckFailure, removeAckReactionHandleAfterReply, shouldAckReactionForWhatsApp } from "openclaw/plugin-sdk/channel-feedback";
20
+ import { createAckReactionHandle, createStatusReactionController, logAckFailure, removeAckReactionHandleAfterReply, shouldAckReaction } from "openclaw/plugin-sdk/channel-feedback";
22
21
  import { formatCliCommand } from "openclaw/plugin-sdk/cli-runtime";
23
- import { getChildLogger } from "openclaw/plugin-sdk/logging-core";
22
+ import { PlatformMessageNotDispatchedError } from "openclaw/plugin-sdk/error-runtime";
23
+ import { createSubsystemLogger, getChildLogger, redactToolPayloadText } from "openclaw/plugin-sdk/logging-core";
24
24
  import { resolveMarkdownTableMode as resolveMarkdownTableMode$1 } from "openclaw/plugin-sdk/markdown-table-runtime";
25
- import { createSubsystemLogger, defaultRuntime, formatDurationPrecise, getChildLogger as getChildLogger$1, logVerbose, logVerbose as logVerbose$1, registerUnhandledRejectionHandler, shouldLogVerbose, shouldLogVerbose as shouldLogVerbose$1, warn } from "openclaw/plugin-sdk/runtime-env";
25
+ import { createSubsystemLogger as createSubsystemLogger$1, defaultRuntime, formatDurationPrecise, getChildLogger as getChildLogger$1, logVerbose, logVerbose as logVerbose$1, registerUnhandledRejectionHandler, shouldLogVerbose, shouldLogVerbose as shouldLogVerbose$1, warn } from "openclaw/plugin-sdk/runtime-env";
26
26
  import { registerChannelRuntimeContext } from "openclaw/plugin-sdk/channel-runtime-context";
27
+ import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
28
+ import { buildChannelInboundEventContext, createChannelPartialDeliveryError, filterChannelInboundQuoteContext, formatInboundEnvelope, formatInboundEnvelope as formatInboundEnvelope$1, formatInboundMediaUnavailableText, formatLocationText, formatMediaPlaceholderText, hasVisibleInboundReplyDispatch, isChannelPartialDeliveryError, readAgentRunTerminalOutcome, resolveInboundSessionEnvelopeContext, resolveInboundSupplementalSenderAllowed, runChannelInboundEvent, shouldDebounceTextInbound, toInboundMediaFactsWithMetadata, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
27
29
  import { bindIngressLifecycleToReplyOptions, createChannelIngressMonitor, createChannelMessageReplyPipeline, createMessageReceiptFromOutboundResults, listMessageReceiptPlatformIds, resolveChannelMessageSourceReplyDeliveryMode, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
28
30
  import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
29
31
  import { isReasoningReplyPayload, resolveSendableOutboundReplyParts, sendMediaWithLeadingCaption } from "openclaw/plugin-sdk/reply-payload";
30
32
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
31
33
  import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
32
- import { buildChannelInboundEventContext, createChannelPartialDeliveryError, filterChannelInboundQuoteContext, formatInboundEnvelope, formatInboundEnvelope as formatInboundEnvelope$1, formatInboundMediaUnavailableText, formatLocationText, formatMediaPlaceholderText, hasVisibleInboundReplyDispatch, isChannelPartialDeliveryError, resolveInboundSessionEnvelopeContext, resolveInboundSupplementalSenderAllowed, runChannelInboundEvent, shouldDebounceTextInbound, toInboundMediaFactsWithMetadata, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
34
+ import { DisconnectReason, downloadMediaMessage, isHostedLidUser, isHostedPnUser, isJidGroup, isJidGroup as isJidGroup$1, jidDecode, jidEncode, jidNormalizedUser, normalizeMessageContent as normalizeMessageContent$1 } from "baileys";
35
+ import { resolveChannelGroupPolicy, resolveChannelGroupRequireMention } from "openclaw/plugin-sdk/channel-policy";
33
36
  import { DEFAULT_ACCOUNT_ID, DEFAULT_MAIN_KEY, buildAgentMainSessionKey, buildAgentSessionKey, buildGroupHistoryKey, deriveLastRoutePolicy, normalizeAccountId, normalizeAgentId, resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
34
37
  import { asDateTimestampMs, parseStrictFiniteNumber, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
38
+ import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
35
39
  import { createHash } from "node:crypto";
36
- import { fanInChannelIngressLifecycles } from "openclaw/plugin-sdk/channel-ingress-runtime";
37
- import { getSessionEntry, patchSessionEntry, resolveStorePath, resolveStorePath as resolveStorePath$1, updateLastRoute } from "openclaw/plugin-sdk/session-store-runtime";
38
- import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
40
+ import { resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
39
41
  import { resolveChunkMode, resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-runtime";
40
42
  import { CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "openclaw/plugin-sdk/approval-handler-runtime";
41
43
  import { createInboundDebouncer, resolveInboundDebounceMs } from "openclaw/plugin-sdk/channel-inbound-debounce";
42
44
  import { drainPendingDeliveries } from "openclaw/plugin-sdk/delivery-queue-runtime";
43
45
  import { DEFAULT_GROUP_HISTORY_LIMIT, buildHistoryContextFromEntries, buildInboundHistoryFromEntries, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
44
46
  import { enqueueSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
47
+ import { fanInChannelIngressLifecycles, resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
48
+ import { getSessionEntry, patchSessionEntry, resolveGroupSessionKey, resolveStorePath, resolveStorePath as resolveStorePath$1, updateLastRoute } from "openclaw/plugin-sdk/session-store-runtime";
45
49
  import { saveMediaStream } from "openclaw/plugin-sdk/media-store";
50
+ import { upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
46
51
  import { createDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
47
52
  import { getRuntimeConfig as getRuntimeConfig$1 } from "openclaw/plugin-sdk/runtime-config-snapshot";
48
53
  import { resolveChannelContextVisibilityMode } from "openclaw/plugin-sdk/context-visibility-runtime";
49
54
  import { buildMentionRegexes, implicitMentionKindWhen, normalizeMentionText, resolveInboundMentionDecision } from "openclaw/plugin-sdk/channel-mention-gating";
50
- import { createConnectedChannelStatusPatch, createTransportActivityStatusPatch } from "openclaw/plugin-sdk/gateway-runtime";
55
+ import { channelReadyPatch, channelStoppedPatch, createTransportActivityStatusPatch } from "openclaw/plugin-sdk/gateway-runtime";
51
56
  import { ensureConfiguredBindingRouteReady, resolveConfiguredBindingRoute } from "openclaw/plugin-sdk/conversation-binding-runtime";
52
57
  import { resolveAgentIdentity, resolveIdentityNamePrefix as resolveIdentityNamePrefix$1 } from "openclaw/plugin-sdk/agent-runtime";
53
58
  import { normalizeGroupActivation, parseActivationCommand } from "openclaw/plugin-sdk/group-activation";
@@ -57,6 +62,241 @@ import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
57
62
  import { resolveBatchedReplyThreadingPolicy } from "openclaw/plugin-sdk/reply-reference";
58
63
  import { LocalMediaAccessError, getDefaultLocalRoots, loadWebMedia, loadWebMediaRaw, optimizeImageToJpeg, optimizeImageToPng } from "openclaw/plugin-sdk/web-media";
59
64
  import { createChannelApiRetryRunner } from "openclaw/plugin-sdk/retry-runtime";
65
+ //#region extensions/whatsapp/src/inbound/group-conversation.ts
66
+ function resolveWhatsAppGroupConversationId(conversationId) {
67
+ return resolveGroupSessionKey({
68
+ From: conversationId,
69
+ ChatType: "group",
70
+ Provider: "whatsapp"
71
+ })?.id ?? conversationId;
72
+ }
73
+ //#endregion
74
+ //#region extensions/whatsapp/src/inbound/admission.ts
75
+ const ingressResolverByAdmission = /* @__PURE__ */ new WeakMap();
76
+ function copyAccount(account) {
77
+ const copied = {
78
+ accountId: account.accountId,
79
+ enabled: account.enabled,
80
+ sendReadReceipts: account.sendReadReceipts
81
+ };
82
+ if (account.name) copied.name = account.name;
83
+ if (typeof account.selfChatMode === "boolean") copied.selfChatMode = account.selfChatMode;
84
+ if (account.replyToMode) copied.replyToMode = account.replyToMode;
85
+ return copied;
86
+ }
87
+ function buildWhatsAppInboundAdmission(params) {
88
+ const admission = {
89
+ channelIngress: params.channelIngress,
90
+ accountId: params.policy.account.accountId,
91
+ isSelfChat: params.policy.isSelfChat,
92
+ account: copyAccount(params.policy.account),
93
+ conversation: {
94
+ kind: params.isGroup ? "group" : "direct",
95
+ id: params.conversationId,
96
+ groupSessionId: resolveWhatsAppGroupConversationId(params.conversationId)
97
+ },
98
+ sender: {
99
+ id: params.senderId,
100
+ isSamePhone: params.policy.isSamePhone(params.senderId)
101
+ },
102
+ ingress: {
103
+ admission: params.access.ingress.admission,
104
+ decision: params.access.ingress.decision,
105
+ decisiveGateId: params.access.ingress.decisiveGateId,
106
+ reasonCode: params.access.ingress.reasonCode
107
+ },
108
+ senderAccess: {
109
+ allowed: params.access.senderAccess.allowed,
110
+ decision: params.access.senderAccess.decision,
111
+ reasonCode: params.access.senderAccess.reasonCode,
112
+ providerMissingFallbackApplied: params.access.senderAccess.providerMissingFallbackApplied
113
+ },
114
+ commandAccess: {
115
+ requested: params.access.commandAccess.requested,
116
+ authorized: params.access.commandAccess.authorized,
117
+ shouldBlockControlCommand: params.access.commandAccess.shouldBlockControlCommand,
118
+ reasonCode: params.access.commandAccess.reasonCode
119
+ },
120
+ activationAccess: {
121
+ ran: params.access.activationAccess.ran,
122
+ allowed: params.access.activationAccess.allowed,
123
+ shouldSkip: params.access.activationAccess.shouldSkip,
124
+ reasonCode: params.access.activationAccess.reasonCode
125
+ }
126
+ };
127
+ if (params.resolveChannelIngress) ingressResolverByAdmission.set(admission, params.resolveChannelIngress);
128
+ return admission;
129
+ }
130
+ async function resolveWhatsAppAdmissionChannelIngress(admission, contextBinding) {
131
+ return await ingressResolverByAdmission.get(admission)?.(contextBinding);
132
+ }
133
+ function buildDeprecatedFlatWhatsAppInboundAdmission(msg) {
134
+ const conversationId = msg.conversationId || msg.from;
135
+ if (!conversationId || !msg.accountId || !msg.chatType) throw new Error("WhatsApp legacy flat inbound messages must include deprecated top-level admission fields.");
136
+ const accountId = msg.accountId;
137
+ const admitted = msg.accessControlPassed !== false;
138
+ const platformSender = msg.platform?.sender;
139
+ const senderE164 = platformSender?.e164 ?? msg.platform?.senderE164 ?? msg.senderE164;
140
+ const senderJid = platformSender?.jid ?? msg.platform?.senderJid ?? msg.senderJid;
141
+ const senderName = platformSender?.name ?? msg.platform?.senderName ?? msg.senderName;
142
+ const senderId = msg.chatType === "group" ? senderE164 ?? senderJid ?? senderName ?? conversationId : senderE164 ?? conversationId;
143
+ const reasonCode = admitted ? msg.chatType === "group" ? "group_policy_allowed" : "dm_policy_allowlisted" : "no_policy_match";
144
+ return buildWhatsAppInboundAdmission({
145
+ policy: {
146
+ account: {
147
+ accountId,
148
+ enabled: true,
149
+ sendReadReceipts: true
150
+ },
151
+ isSelfChat: false,
152
+ isSamePhone: () => false
153
+ },
154
+ access: {
155
+ ingress: {
156
+ admission: admitted ? "dispatch" : "drop",
157
+ decision: admitted ? "allow" : "block",
158
+ decisiveGateId: "legacy-flat-compat",
159
+ reasonCode
160
+ },
161
+ senderAccess: {
162
+ allowed: admitted,
163
+ decision: admitted ? "allow" : "block",
164
+ reasonCode,
165
+ providerMissingFallbackApplied: false
166
+ },
167
+ commandAccess: {
168
+ requested: false,
169
+ authorized: false,
170
+ shouldBlockControlCommand: false,
171
+ reasonCode: "command_authorized"
172
+ },
173
+ activationAccess: {
174
+ ran: false,
175
+ allowed: admitted,
176
+ shouldSkip: !admitted,
177
+ reasonCode: admitted ? "activation_allowed" : "activation_skipped"
178
+ }
179
+ },
180
+ isGroup: msg.chatType === "group",
181
+ conversationId,
182
+ senderId
183
+ });
184
+ }
185
+ function requireWhatsAppInboundAdmission(params) {
186
+ if (!params.admission) throw new Error("WhatsApp inbound message is missing admission facts");
187
+ return params.admission;
188
+ }
189
+ function requireAdmittedWhatsAppInboundMessage(msg) {
190
+ requireWhatsAppInboundAdmission(msg);
191
+ return msg;
192
+ }
193
+ //#endregion
194
+ //#region extensions/whatsapp/src/inbound-policy.ts
195
+ function normalizeWhatsAppIngressPhone(value) {
196
+ const trimmed = value.trim();
197
+ if (!trimmed) return null;
198
+ return normalizeE164(trimmed);
199
+ }
200
+ function buildResolvedWhatsAppGroupConfig(params) {
201
+ return { channels: { whatsapp: {
202
+ groupPolicy: params.groupPolicy,
203
+ groups: params.groups
204
+ } } };
205
+ }
206
+ function resolveWhatsAppInboundPolicy(params) {
207
+ const account = resolveWhatsAppAccount({
208
+ cfg: params.cfg,
209
+ accountId: params.accountId
210
+ });
211
+ const configuredAllowFrom = account.allowFrom ?? [];
212
+ const dmPolicy = account.dmPolicy ?? "pairing";
213
+ const dmAllowFrom = configuredAllowFrom.length > 0 ? configuredAllowFrom : params.selfE164 ? [params.selfE164] : [];
214
+ const groupAllowFrom = (Array.isArray(account.groupAllowFrom) && account.groupAllowFrom.length > 0 ? account.groupAllowFrom : void 0) ?? (configuredAllowFrom.length > 0 ? configuredAllowFrom : void 0) ?? [];
215
+ const defaultGroupPolicy = resolveDefaultGroupPolicy(params.cfg);
216
+ const { groupPolicy, providerMissingFallbackApplied } = resolveWhatsAppRuntimeGroupPolicy({
217
+ providerConfigPresent: params.cfg.channels?.whatsapp !== void 0,
218
+ groupPolicy: account.groupPolicy,
219
+ defaultGroupPolicy
220
+ });
221
+ const resolvedGroupCfg = buildResolvedWhatsAppGroupConfig({
222
+ groupPolicy,
223
+ groups: account.groups
224
+ });
225
+ const isSamePhone = (value) => typeof value === "string" && typeof params.selfE164 === "string" && value === params.selfE164;
226
+ return {
227
+ account,
228
+ dmPolicy,
229
+ groupPolicy,
230
+ configuredAllowFrom,
231
+ dmAllowFrom,
232
+ groupAllowFrom,
233
+ isSelfChat: account.selfChatMode ?? isSelfChatMode(params.selfE164, configuredAllowFrom),
234
+ providerMissingFallbackApplied,
235
+ isSamePhone,
236
+ resolveConversationGroupPolicy: (conversationId) => resolveChannelGroupPolicy({
237
+ cfg: resolvedGroupCfg,
238
+ channel: "whatsapp",
239
+ groupId: resolveWhatsAppGroupConversationId(conversationId),
240
+ hasGroupAllowFrom: groupAllowFrom.length > 0
241
+ }),
242
+ resolveConversationRequireMention: (conversationId) => resolveChannelGroupRequireMention({
243
+ cfg: resolvedGroupCfg,
244
+ channel: "whatsapp",
245
+ groupId: resolveWhatsAppGroupConversationId(conversationId)
246
+ })
247
+ };
248
+ }
249
+ async function resolveWhatsAppIngressAccess(params) {
250
+ return await resolveStableChannelMessageIngress({
251
+ channelId: "whatsapp",
252
+ accountId: params.policy.account.accountId,
253
+ identity: {
254
+ key: "whatsapp-sender-phone",
255
+ kind: "phone",
256
+ normalize: normalizeWhatsAppIngressPhone,
257
+ sensitivity: "pii",
258
+ entryIdPrefix: "whatsapp-entry"
259
+ },
260
+ cfg: params.cfg,
261
+ useDefaultPairingStore: true,
262
+ subject: { stableId: params.senderId ?? "" },
263
+ conversation: {
264
+ kind: params.isGroup ? "group" : "direct",
265
+ id: params.conversationId
266
+ },
267
+ contextBinding: params.contextBinding,
268
+ dmPolicy: params.policy.dmPolicy,
269
+ groupPolicy: params.policy.groupPolicy,
270
+ policy: { groupAllowFromFallbackToAllowFrom: false },
271
+ providerMissingFallbackApplied: params.policy.providerMissingFallbackApplied,
272
+ allowFrom: !params.isGroup && params.policy.account.selfChatMode !== false && params.senderId && params.policy.isSamePhone(params.senderId) ? [...params.policy.dmAllowFrom, params.senderId] : params.policy.dmAllowFrom,
273
+ groupAllowFrom: params.policy.groupAllowFrom,
274
+ command: params.includeCommand === true ? {} : void 0
275
+ });
276
+ }
277
+ async function resolveWhatsAppCommandAuthorized(params) {
278
+ const self = getSelfIdentity(params.msg, params.authDir);
279
+ const admission = requireWhatsAppInboundAdmission(params.msg);
280
+ const policy = params.policy ?? resolveWhatsAppInboundPolicy({
281
+ cfg: params.cfg,
282
+ accountId: admission.accountId,
283
+ selfE164: self.e164 ?? null
284
+ });
285
+ const isGroup = admission.conversation.kind === "group";
286
+ const sender = getSenderIdentity(params.msg, params.authDir);
287
+ const dmSender = sender.e164 ?? admission.conversation.id;
288
+ const groupSender = sender.e164 ?? "";
289
+ if (!normalizeE164(isGroup ? groupSender : dmSender)) return false;
290
+ return (await resolveWhatsAppIngressAccess({
291
+ cfg: params.cfg,
292
+ policy,
293
+ isGroup,
294
+ conversationId: admission.conversation.id,
295
+ senderId: isGroup ? groupSender : dmSender,
296
+ includeCommand: true
297
+ })).commandAccess.authorized;
298
+ }
299
+ //#endregion
60
300
  //#region extensions/whatsapp/src/inbound/baileys-cache.ts
61
301
  const WHATSAPP_BAILEYS_CACHE_MAX_ENTRIES = 500;
62
302
  function rememberWhatsAppBaileysCacheEntry(cache, key, value, ttlMs) {
@@ -625,10 +865,14 @@ function createWhatsAppGroupMetadataCacheOwner(params) {
625
865
  const cached = readGroupMetadataCacheEntry(localCache, jid);
626
866
  if (cached) return cached;
627
867
  try {
628
- const meta = await (params.getCurrentSock() ?? params.sock).groupMetadata(jid);
629
- rememberWhatsAppBaileysCacheEntry(params.baileysCache, jid, meta, GROUP_META_TTL_MS);
868
+ const hydratedEntry = params.baileysCache?.get(jid);
869
+ const providerMetadata = params.baileysCache ? readWhatsAppBaileysCacheEntry(params.baileysCache, jid) : void 0;
870
+ const hydratedMetadata = providerMetadata?.participants?.length ? providerMetadata : void 0;
871
+ const meta = hydratedMetadata ?? await (params.getCurrentSock() ?? params.sock).groupMetadata(jid);
872
+ if (!hydratedMetadata) rememberWhatsAppBaileysCacheEntry(params.baileysCache, jid, meta, GROUP_META_TTL_MS);
630
873
  publishedJids.add(jid);
631
874
  const entry = await summarize(meta);
875
+ if (hydratedMetadata && hydratedEntry) entry.expires = hydratedEntry.expiresAt;
632
876
  rememberGroupMetadataCacheEntry(reconnectCache, jid, {
633
877
  subject: entry.subject,
634
878
  expires: entry.expires
@@ -647,7 +891,7 @@ function createWhatsAppGroupMetadataCacheOwner(params) {
647
891
  }
648
892
  };
649
893
  const resolveOutboundMentions = async (jid, text) => {
650
- if (isJidGroup(jid) !== true || !mayContainWhatsAppOutboundMention(text)) return {
894
+ if (isJidGroup$1(jid) !== true || !mayContainWhatsAppOutboundMention(text)) return {
651
895
  text,
652
896
  mentionedJids: []
653
897
  };
@@ -898,27 +1142,21 @@ function createWhatsAppInboundMessageDebouncer(options) {
898
1142
  }
899
1143
  //#endregion
900
1144
  //#region extensions/whatsapp/src/inbound/media.ts
901
- var WhatsAppInboundMediaLimitExceededError = class extends Error {
902
- constructor(maxBytes) {
903
- super(`Media exceeds ${Math.round(maxBytes / (1024 * 1024))}MB limit`);
904
- this.name = "WhatsAppInboundMediaLimitExceededError";
905
- }
906
- };
907
1145
  function unwrapMessage(message) {
908
- return normalizeMessageContent(message);
1146
+ return normalizeMessageContent$1(message);
909
1147
  }
910
1148
  async function downloadInboundMedia(msg, sock, maxBytes = 50 * 1024 * 1024) {
911
1149
  const message = unwrapMessage(msg.message);
912
1150
  if (!message) return;
913
1151
  const mimetype = resolveInboundMediaMimetype(message);
914
1152
  const fileName = message.documentMessage?.fileName ?? void 0;
915
- if (!message.imageMessage && !message.videoMessage && !message.documentMessage && !message.audioMessage && !message.stickerMessage) return;
1153
+ if (!message.imageMessage && !message.videoMessage && !message.ptvMessage && !message.documentMessage && !message.audioMessage && !message.stickerMessage) return;
916
1154
  return {
917
1155
  saved: await saveMediaStream(await downloadMediaMessage(msg, "stream", {}, {
918
1156
  reuploadRequest: sock.updateMediaMessage,
919
1157
  logger: sock.logger
920
1158
  }), mimetype, "inbound", maxBytes, fileName).catch((err) => {
921
- if (err instanceof Error && /Media exceeds/i.test(err.message)) throw new WhatsAppInboundMediaLimitExceededError(maxBytes);
1159
+ if (err instanceof Error && /Media exceeds/i.test(err.message)) throw new Error(`Media exceeds ${Math.round(maxBytes / (1024 * 1024))}MB limit`);
922
1160
  throw err;
923
1161
  }),
924
1162
  mimetype,
@@ -929,12 +1167,18 @@ async function downloadQuotedInboundMedia(msg, sock, maxBytes = 50 * 1024 * 1024
929
1167
  const contextInfo = extractContextInfo(unwrapMessage(msg.message));
930
1168
  if (!contextInfo?.quotedMessage) return;
931
1169
  const quotedMessage = contextInfo.quotedMessage;
1170
+ const self = sock.user;
1171
+ const quotedFromMe = identitiesOverlap({ jid: contextInfo.participant }, {
1172
+ jid: self?.id,
1173
+ lid: self?.lid,
1174
+ e164: self?.phoneNumber
1175
+ });
932
1176
  return downloadInboundMedia({
933
1177
  key: {
934
1178
  id: contextInfo?.stanzaId || void 0,
935
1179
  remoteJid: contextInfo.remoteJid ?? msg.key?.remoteJid ?? void 0,
936
1180
  participant: contextInfo?.participant ?? void 0,
937
- fromMe: false
1181
+ fromMe: quotedFromMe
938
1182
  },
939
1183
  message: quotedMessage,
940
1184
  messageTimestamp: msg.messageTimestamp
@@ -942,6 +1186,32 @@ async function downloadQuotedInboundMedia(msg, sock, maxBytes = 50 * 1024 * 1024
942
1186
  }
943
1187
  //#endregion
944
1188
  //#region extensions/whatsapp/src/inbound/message-enrichment.ts
1189
+ const inboundMediaLogger = createSubsystemLogger("gateway/channels/whatsapp").child("inbound");
1190
+ const MAX_MEDIA_ERROR_MESSAGE_CHARS = 256;
1191
+ function sanitizeMediaErrorMessage(error) {
1192
+ return truncateUtf16Safe(redactToolPayloadText(error instanceof Error ? error.message : String(error)).replace(/https?:\/\/\S+/giu, "[redacted-url]").replace(/\b[^@\s]+@[a-z][a-z\d.-]*\b/giu, "[redacted-jid]").replace(/\+?\d[\d ().-]{6,}\d/gu, "[redacted-phone]").split("\n", 1)[0]?.trim() || "unknown error", MAX_MEDIA_ERROR_MESSAGE_CHARS);
1193
+ }
1194
+ function logMediaMaterializationFailure(params) {
1195
+ const errorClass = params.error instanceof Error && /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/u.test(params.error.name) ? params.error.name : "Error";
1196
+ const statusCode = getStatusCode(params.error);
1197
+ const errorMessage = sanitizeMediaErrorMessage(params.error);
1198
+ const failureDetails = [
1199
+ `stage=${params.failureStage}`,
1200
+ ...params.mediaKind ? [`kind=${params.mediaKind}`] : [],
1201
+ ...typeof statusCode === "number" ? [`status=${statusCode}`] : []
1202
+ ].join(" ");
1203
+ inboundMediaLogger.warn("WhatsApp inbound media materialization failed", {
1204
+ channel: "whatsapp",
1205
+ messageId: params.messageId ?? void 0,
1206
+ mediaKind: params.mediaKind,
1207
+ mimeType: params.mimeType,
1208
+ failureStage: params.failureStage,
1209
+ errorClass,
1210
+ ...typeof statusCode === "number" ? { statusCode } : {},
1211
+ errorMessage,
1212
+ consoleMessage: `WhatsApp inbound media materialization failed (${failureDetails}): ${errorMessage}`
1213
+ });
1214
+ }
945
1215
  async function enrichWhatsAppInboundMessage(params) {
946
1216
  const { msg, sock } = params;
947
1217
  const location = extractLocationData(msg.message ?? void 0);
@@ -972,6 +1242,13 @@ async function enrichWhatsAppInboundMessage(params) {
972
1242
  try {
973
1243
  await saveInboundMedia(await downloadInboundMedia(msg, sock, maxBytes));
974
1244
  } catch (error) {
1245
+ logMediaMaterializationFailure({
1246
+ messageId: msg.key?.id,
1247
+ mediaKind,
1248
+ mimeType: mediaType,
1249
+ failureStage: "direct",
1250
+ error
1251
+ });
975
1252
  params.logVerbose(`Inbound media download failed: ${String(error)}`);
976
1253
  body = formatInboundMediaUnavailableText({
977
1254
  body,
@@ -983,6 +1260,13 @@ async function enrichWhatsAppInboundMessage(params) {
983
1260
  mediaKind = replyContext.media.kind ?? void 0;
984
1261
  mediaType = mediaType ?? replyContext.media.contentType ?? void 0;
985
1262
  } catch (error) {
1263
+ logMediaMaterializationFailure({
1264
+ messageId: msg.key?.id,
1265
+ mediaKind: replyContext.media.kind ?? void 0,
1266
+ mimeType: replyContext.media.contentType ?? void 0,
1267
+ failureStage: "quoted",
1268
+ error
1269
+ });
986
1270
  params.logVerbose(`Quoted media download failed: ${String(error)}`);
987
1271
  body = formatInboundMediaUnavailableText({
988
1272
  body,
@@ -1004,6 +1288,112 @@ async function enrichWhatsAppInboundMessage(params) {
1004
1288
  };
1005
1289
  }
1006
1290
  //#endregion
1291
+ //#region extensions/whatsapp/src/inbound/access-control.ts
1292
+ const PAIRING_REPLY_HISTORY_GRACE_MS = 3e4;
1293
+ function logWhatsAppVerbose$2(enabled, message) {
1294
+ if (!enabled) return;
1295
+ defaultRuntime.log(message);
1296
+ }
1297
+ function blockedInboundAccess(policy) {
1298
+ return {
1299
+ allowed: false,
1300
+ shouldMarkRead: false,
1301
+ isSelfChat: policy.isSelfChat,
1302
+ resolvedAccountId: policy.account.accountId
1303
+ };
1304
+ }
1305
+ async function checkInboundAccessControl(params) {
1306
+ const policy = resolveWhatsAppInboundPolicy({
1307
+ cfg: params.cfg,
1308
+ accountId: params.accountId,
1309
+ selfE164: params.selfE164
1310
+ });
1311
+ const pairingGraceMs = typeof params.pairingGraceMs === "number" && params.pairingGraceMs > 0 ? params.pairingGraceMs : PAIRING_REPLY_HISTORY_GRACE_MS;
1312
+ const suppressPairingReply = typeof params.connectedAtMs === "number" && typeof params.messageTimestampMs === "number" && params.messageTimestampMs < params.connectedAtMs - pairingGraceMs;
1313
+ warnMissingProviderGroupPolicyFallbackOnce({
1314
+ providerMissingFallbackApplied: policy.providerMissingFallbackApplied,
1315
+ providerKey: "whatsapp",
1316
+ accountId: policy.account.accountId,
1317
+ log: (message) => logWhatsAppVerbose$2(params.verbose, message)
1318
+ });
1319
+ const conversationId = params.group ? params.remoteJid : params.from;
1320
+ const accessSenderId = params.group ? params.senderE164 : params.from;
1321
+ const admissionSenderId = params.group ? params.senderE164 ?? params.senderJid ?? params.from : params.from;
1322
+ const resolveChannelIngress = async (contextBinding) => await resolveWhatsAppIngressAccess({
1323
+ cfg: params.cfg,
1324
+ policy,
1325
+ isGroup: params.group,
1326
+ conversationId,
1327
+ senderId: accessSenderId,
1328
+ contextBinding
1329
+ });
1330
+ const access = await resolveChannelIngress();
1331
+ const { senderAccess } = access;
1332
+ if (params.group && senderAccess.decision !== "allow") {
1333
+ if (senderAccess.reasonCode === "group_policy_disabled") logWhatsAppVerbose$2(params.verbose, "Blocked group message (groupPolicy: disabled)");
1334
+ else if (senderAccess.reasonCode === "group_policy_empty_allowlist") logWhatsAppVerbose$2(params.verbose, "Blocked group message (groupPolicy: allowlist, no groupAllowFrom)");
1335
+ else logWhatsAppVerbose$2(params.verbose, `Blocked group message from ${params.senderE164 ?? "unknown sender"} (groupPolicy: allowlist)`);
1336
+ return blockedInboundAccess(policy);
1337
+ }
1338
+ if (!params.group) {
1339
+ if (params.isFromMe && (policy.account.selfChatMode === false || !policy.isSamePhone(params.from))) {
1340
+ logWhatsAppVerbose$2(params.verbose, "Skipping outbound DM (fromMe); no pairing reply needed.");
1341
+ return blockedInboundAccess(policy);
1342
+ }
1343
+ if (senderAccess.decision === "block" && senderAccess.reasonCode === "dm_policy_disabled") {
1344
+ logWhatsAppVerbose$2(params.verbose, "Blocked dm (dmPolicy: disabled)");
1345
+ return blockedInboundAccess(policy);
1346
+ }
1347
+ if (senderAccess.decision === "pairing" && !policy.isSamePhone(params.from)) {
1348
+ const candidate = params.from;
1349
+ if (suppressPairingReply) logWhatsAppVerbose$2(params.verbose, `Skipping pairing reply for historical DM from ${candidate}.`);
1350
+ else await createChannelPairingChallengeIssuer({
1351
+ channel: "whatsapp",
1352
+ accountId: policy.account.accountId,
1353
+ upsertPairingRequest: async ({ id, meta }) => await upsertChannelPairingRequest({
1354
+ channel: "whatsapp",
1355
+ id,
1356
+ accountId: policy.account.accountId,
1357
+ meta
1358
+ })
1359
+ })({
1360
+ senderId: candidate,
1361
+ senderIdLine: `Your WhatsApp phone number: ${candidate}`,
1362
+ meta: { name: (params.pushName ?? "").trim() || void 0 },
1363
+ onCreated: () => {
1364
+ logWhatsAppVerbose$2(params.verbose, `whatsapp pairing request sender=${candidate} name=${params.pushName ?? "unknown"}`);
1365
+ },
1366
+ sendPairingReply: async (text) => {
1367
+ await params.sock.sendMessage(params.remoteJid, { text });
1368
+ },
1369
+ onReplyError: (err) => {
1370
+ logWhatsAppVerbose$2(params.verbose, `whatsapp pairing reply failed for ${candidate}: ${String(err)}`);
1371
+ }
1372
+ });
1373
+ return blockedInboundAccess(policy);
1374
+ }
1375
+ if (senderAccess.decision !== "allow") {
1376
+ logWhatsAppVerbose$2(params.verbose, `Blocked unauthorized sender ${params.from} (dmPolicy=${policy.dmPolicy})`);
1377
+ return blockedInboundAccess(policy);
1378
+ }
1379
+ }
1380
+ return {
1381
+ allowed: true,
1382
+ shouldMarkRead: true,
1383
+ isSelfChat: policy.isSelfChat,
1384
+ resolvedAccountId: policy.account.accountId,
1385
+ admission: buildWhatsAppInboundAdmission({
1386
+ policy,
1387
+ access,
1388
+ channelIngress: access,
1389
+ resolveChannelIngress,
1390
+ isGroup: params.group,
1391
+ conversationId,
1392
+ senderId: admissionSenderId
1393
+ })
1394
+ };
1395
+ }
1396
+ //#endregion
1007
1397
  //#region extensions/whatsapp/src/inbound/dedupe.ts
1008
1398
  const recentOutboundMessages = createDedupeCache({
1009
1399
  ttlMs: 20 * 6e4,
@@ -1011,7 +1401,14 @@ const recentOutboundMessages = createDedupeCache({
1011
1401
  });
1012
1402
  function buildMessageKey(params) {
1013
1403
  const accountId = params.accountId.trim();
1014
- const remoteJid = params.remoteJid.trim();
1404
+ const rawRemoteJid = params.remoteJid.trim();
1405
+ const hostedPhone = isHostedPnUser(rawRemoteJid);
1406
+ let remoteJid;
1407
+ if (hostedPhone || isHostedLidUser(rawRemoteJid)) {
1408
+ const decodedJid = jidDecode(rawRemoteJid);
1409
+ if (!decodedJid) return null;
1410
+ remoteJid = jidEncode(decodedJid.user, hostedPhone ? "s.whatsapp.net" : "lid");
1411
+ } else remoteJid = jidNormalizedUser(rawRemoteJid);
1015
1412
  const messageId = params.messageId.trim();
1016
1413
  if (!accountId || !remoteJid || !messageId || messageId === "unknown") return null;
1017
1414
  return `${accountId}:${remoteJid}:${messageId}`;
@@ -1027,7 +1424,13 @@ function rememberRecentOutboundMessage(params) {
1027
1424
  function isRecentOutboundMessage(params) {
1028
1425
  const key = buildMessageKey(params);
1029
1426
  if (!key) return false;
1030
- return recentOutboundMessages.peek(key);
1427
+ if (recentOutboundMessages.peek(key)) return true;
1428
+ if (!params.alternateRemoteJid || isJidGroup(params.remoteJid)) return false;
1429
+ const alternateKey = buildMessageKey({
1430
+ ...params,
1431
+ remoteJid: params.alternateRemoteJid
1432
+ });
1433
+ return alternateKey !== null && recentOutboundMessages.peek(alternateKey);
1031
1434
  }
1032
1435
  //#endregion
1033
1436
  //#region extensions/whatsapp/src/inbound/message-normalization.ts
@@ -1039,6 +1442,7 @@ function createWhatsAppInboundMessageNormalizer(options) {
1039
1442
  if (!msg.key?.fromMe || !id || !remoteJid || !isRecentOutboundMessage({
1040
1443
  accountId: options.accountId,
1041
1444
  remoteJid,
1445
+ alternateRemoteJid: msg.key?.remoteJidAlt,
1042
1446
  messageId: id
1043
1447
  })) return false;
1044
1448
  options.logVerbose(`Skipping recent outbound WhatsApp echo ${id} for ${remoteJid}`);
@@ -1048,7 +1452,7 @@ function createWhatsAppInboundMessageNormalizer(options) {
1048
1452
  const id = msg.key?.id ?? void 0;
1049
1453
  const remoteJid = msg.key?.remoteJid;
1050
1454
  if (!remoteJid || remoteJid.endsWith("@status") || remoteJid.endsWith("@broadcast")) return null;
1051
- const group = isJidGroup(remoteJid) === true;
1455
+ const group = isJidGroup$1(remoteJid) === true;
1052
1456
  if (shouldSkipRecentOutboundEcho(msg)) return null;
1053
1457
  if (!hasInboundUserContent(msg.message ?? void 0)) return null;
1054
1458
  const participantJid = msg.key?.participant ?? void 0;
@@ -1122,7 +1526,7 @@ function recordAcceptedInboundActivity(accountId) {
1122
1526
  }
1123
1527
  function createWhatsAppMessageDeliveryCoordinator(options) {
1124
1528
  const inboundLogger = getChildLogger({ module: "web-inbound" });
1125
- const inboundConsoleLog = createSubsystemLogger("gateway/channels/whatsapp").child("inbound");
1529
+ const inboundConsoleLog = createSubsystemLogger$1("gateway/channels/whatsapp").child("inbound");
1126
1530
  const sock = options.sock;
1127
1531
  const socketSession = options.socketSession;
1128
1532
  const groupMetadata = options.groupMetadata;
@@ -1634,7 +2038,10 @@ async function createWhatsAppAttachedSocketSession(options) {
1634
2038
  if (readinessOptions?.useVerifiedReady && consumeVerifiedSendReady(jid, currentSock)) return;
1635
2039
  const state = getActiveReachoutTimelock(reachoutTimeLock) ?? await fetchReachoutTimeLock(currentSock);
1636
2040
  const activeState = getActiveReachoutTimelock(state);
1637
- if (activeState) throw new Error(formatReachoutTimelockError(activeState));
2041
+ if (activeState) {
2042
+ const error = new Error(formatReachoutTimelockError(activeState));
2043
+ throw readinessOptions?.rememberReady ? new PlatformMessageNotDispatchedError(error.message, { cause: error }) : error;
2044
+ }
1638
2045
  if (readinessOptions?.rememberReady && state) rememberVerifiedSendReady(jid, currentSock);
1639
2046
  };
1640
2047
  const assertSendReady = async (to) => {
@@ -1757,7 +2164,7 @@ function logWhatsAppVerbose(enabled, message) {
1757
2164
  }
1758
2165
  async function attachWebInboxToSocket(options) {
1759
2166
  const inboundLogger = getChildLogger({ module: "web-inbound" });
1760
- const inboundConsoleLog = createSubsystemLogger("gateway/channels/whatsapp").child("inbound");
2167
+ const inboundConsoleLog = createSubsystemLogger$1("gateway/channels/whatsapp").child("inbound");
1761
2168
  const socketSession = await createWhatsAppAttachedSocketSession({
1762
2169
  sock: options.sock,
1763
2170
  socketRef: options.socketRef,
@@ -1868,7 +2275,7 @@ async function monitorWebInbox(options) {
1868
2275
  }
1869
2276
  //#endregion
1870
2277
  //#region extensions/whatsapp/src/auto-reply/loggers.ts
1871
- const whatsappLog = createSubsystemLogger("gateway/channels/whatsapp");
2278
+ const whatsappLog = createSubsystemLogger$1("gateway/channels/whatsapp");
1872
2279
  const whatsappInboundLog = whatsappLog.child("inbound");
1873
2280
  const whatsappOutboundLog = whatsappLog.child("outbound");
1874
2281
  const whatsappHeartbeatLog = whatsappLog.child("heartbeat");
@@ -1932,6 +2339,15 @@ function resolveOwnerList(mentionCfg, selfE164) {
1932
2339
  }
1933
2340
  //#endregion
1934
2341
  //#region extensions/whatsapp/src/auto-reply/monitor-state.ts
2342
+ const LIFECYCLE_BY_HEALTH_STATE = {
2343
+ starting: "starting",
2344
+ healthy: "ready",
2345
+ stale: "recovering",
2346
+ reconnecting: "recovering",
2347
+ conflict: "blocked",
2348
+ "logged-out": "blocked",
2349
+ stopped: "blocked"
2350
+ };
1935
2351
  function cloneStatus(status) {
1936
2352
  return {
1937
2353
  ...status,
@@ -1955,7 +2371,8 @@ function createWebChannelStatusController(statusSink) {
1955
2371
  lastError: null,
1956
2372
  busy: false,
1957
2373
  lastRunActivityAt: null,
1958
- healthState: "starting"
2374
+ healthState: "starting",
2375
+ lifecycle: "starting"
1959
2376
  };
1960
2377
  const emit = () => {
1961
2378
  statusSink?.(cloneStatus(status));
@@ -1964,16 +2381,17 @@ function createWebChannelStatusController(statusSink) {
1964
2381
  emit,
1965
2382
  snapshot: () => status,
1966
2383
  noteConnected(at = Date.now()) {
1967
- Object.assign(status, createConnectedChannelStatusPatch(at));
2384
+ Object.assign(status, channelReadyPatch({
2385
+ lastConnectedAt: at,
2386
+ lastEventAt: at
2387
+ }));
1968
2388
  Object.assign(status, createTransportActivityStatusPatch(at));
1969
2389
  if (lastDisconnectWasWatchdogRecovery) {
1970
2390
  status.lastDisconnect = null;
1971
2391
  status.reconnectAttempts = 0;
1972
2392
  lastDisconnectWasWatchdogRecovery = false;
1973
2393
  }
1974
- status.lastError = null;
1975
2394
  status.healthState = "healthy";
1976
- status.terminalDisconnect = void 0;
1977
2395
  emit();
1978
2396
  },
1979
2397
  noteInbound(at = Date.now()) {
@@ -1981,7 +2399,10 @@ function createWebChannelStatusController(statusSink) {
1981
2399
  status.lastMessageAt = at;
1982
2400
  status.lastEventAt = at;
1983
2401
  Object.assign(status, createTransportActivityStatusPatch(at));
1984
- if (status.connected) status.healthState = "healthy";
2402
+ if (status.connected) {
2403
+ status.healthState = "healthy";
2404
+ status.lifecycle = "ready";
2405
+ }
1985
2406
  emit();
1986
2407
  },
1987
2408
  noteTransportActivity(at = Date.now()) {
@@ -1993,12 +2414,18 @@ function createWebChannelStatusController(statusSink) {
1993
2414
  if (status.busy === busy && status.lastRunActivityAt === at) return;
1994
2415
  status.busy = busy;
1995
2416
  status.lastRunActivityAt = at;
1996
- if (status.connected && busy) status.healthState = "healthy";
2417
+ if (status.connected && busy) {
2418
+ status.healthState = "healthy";
2419
+ status.lifecycle = "ready";
2420
+ }
1997
2421
  emit();
1998
2422
  },
1999
2423
  noteWatchdogStale(at = Date.now()) {
2000
2424
  status.lastEventAt = at;
2001
- if (status.connected) status.healthState = "stale";
2425
+ if (status.connected) {
2426
+ status.healthState = "stale";
2427
+ status.lifecycle = "recovering";
2428
+ }
2002
2429
  emit();
2003
2430
  },
2004
2431
  noteReconnectAttempts(reconnectAttempts) {
@@ -2019,52 +2446,28 @@ function createWebChannelStatusController(statusSink) {
2019
2446
  status.lastError = params.error ?? null;
2020
2447
  status.reconnectAttempts = params.reconnectAttempts;
2021
2448
  status.healthState = params.healthState;
2449
+ status.lifecycle = LIFECYCLE_BY_HEALTH_STATE[params.healthState];
2022
2450
  emit();
2023
2451
  },
2024
2452
  markStopped(at = Date.now()) {
2025
- status.running = false;
2026
- status.connected = false;
2027
- status.lastEventAt = at;
2028
- status.terminalDisconnect = status.healthState === "logged-out" || status.healthState === "conflict";
2029
- if (!isTerminalHealthState(status.healthState)) status.healthState = "stopped";
2453
+ const terminalDisconnect = status.lifecycle === "blocked";
2454
+ if (!isTerminalHealthState(status.healthState)) {
2455
+ Object.assign(status, channelStoppedPatch({
2456
+ lastEventAt: at,
2457
+ terminalDisconnect
2458
+ }));
2459
+ status.healthState = "stopped";
2460
+ } else Object.assign(status, {
2461
+ running: false,
2462
+ connected: false,
2463
+ lastEventAt: at,
2464
+ terminalDisconnect
2465
+ });
2030
2466
  emit();
2031
2467
  }
2032
2468
  };
2033
2469
  }
2034
2470
  //#endregion
2035
- //#region extensions/whatsapp/src/auto-reply/monitor/echo.ts
2036
- function createEchoTracker(params) {
2037
- const recentlySent = /* @__PURE__ */ new Set();
2038
- const maxItems = Math.max(1, params.maxItems ?? 100);
2039
- const buildCombinedKey = (p) => `combined:${p.sessionKey}:${p.combinedBody}`;
2040
- const buildTextKey = (text, conversationId) => conversationId ? `${conversationId}\0${text}` : text;
2041
- const trim = () => {
2042
- while (recentlySent.size > maxItems) {
2043
- const firstKey = recentlySent.values().next().value;
2044
- if (!firstKey) break;
2045
- recentlySent.delete(firstKey);
2046
- }
2047
- };
2048
- const rememberText = (text, opts) => {
2049
- if (!text) return;
2050
- recentlySent.add(buildTextKey(text, opts.conversationId));
2051
- if (opts.combinedBody && opts.combinedBodySessionKey) recentlySent.add(buildCombinedKey({
2052
- sessionKey: opts.combinedBodySessionKey,
2053
- combinedBody: opts.combinedBody
2054
- }));
2055
- if (opts.logVerboseMessage) params.logVerbose?.(`Added to echo detection set (size now: ${recentlySent.size}): ${truncateUtf16Safe(text, 50)}...`);
2056
- trim();
2057
- };
2058
- return {
2059
- rememberText,
2060
- has: (key, conversationId) => recentlySent.has(buildTextKey(key, conversationId)),
2061
- forget: (key, conversationId) => {
2062
- recentlySent.delete(buildTextKey(key, conversationId));
2063
- },
2064
- buildCombinedKey
2065
- };
2066
- }
2067
- //#endregion
2068
2471
  //#region extensions/whatsapp/src/auto-reply/monitor/listener-log.ts
2069
2472
  function formatWhatsAppInboundListeningLog(account) {
2070
2473
  if (account.groupPolicy === "disabled") return "Listening for WhatsApp inbound messages (DM + groups disabled by groupPolicy).";
@@ -2157,8 +2560,6 @@ async function resolveWhatsAppReactionEligibility(params) {
2157
2560
  agentId: params.agentId,
2158
2561
  ackConfig: params.cfg.messages?.ackReaction
2159
2562
  });
2160
- const directEnabled = scope === "all" || scope === "direct";
2161
- const groupMode = scope === "all" || scope === "group-all" ? "always" : scope === "direct" ? "never" : "mentions";
2162
2563
  const isGroup = admission.conversation.kind === "group";
2163
2564
  const activation = isGroup ? await resolveGroupActivationFor({
2164
2565
  cfg: params.cfg,
@@ -2167,14 +2568,14 @@ async function resolveWhatsAppReactionEligibility(params) {
2167
2568
  sessionKey: params.sessionKey,
2168
2569
  conversationId: admission.conversation.id
2169
2570
  }) : null;
2170
- if (!shouldAckReactionForWhatsApp({
2171
- emoji,
2571
+ if (!emoji || !shouldAckReaction({
2572
+ scope,
2172
2573
  isDirect: admission.conversation.kind === "direct",
2173
2574
  isGroup,
2174
- directEnabled,
2175
- groupMode,
2176
- wasMentioned: (params.msg.groupMention?.wasMentioned ?? params.msg.wasMentioned) === true,
2177
- groupActivated: activation === "always"
2575
+ isMentionableGroup: isGroup,
2576
+ canDetectMention: isGroup,
2577
+ effectiveWasMentioned: (params.msg.groupMention?.wasMentioned ?? params.msg.wasMentioned) === true,
2578
+ shouldBypassMention: activation === "always"
2178
2579
  })) return DISABLED_REACTION;
2179
2580
  const participant = resolveReactionParticipant(params.msg);
2180
2581
  return {
@@ -2184,7 +2585,7 @@ async function resolveWhatsAppReactionEligibility(params) {
2184
2585
  emoji,
2185
2586
  reactionOptions: {
2186
2587
  verbose: params.verbose,
2187
- fromMe: false,
2588
+ fromMe: params.msg.platform.fromMe === true,
2188
2589
  ...participant ? { participant } : {},
2189
2590
  accountId,
2190
2591
  cfg: params.cfg
@@ -2299,6 +2700,11 @@ async function maybeBroadcastMessage(params) {
2299
2700
  return true;
2300
2701
  }
2301
2702
  //#endregion
2703
+ //#region extensions/whatsapp/src/auto-reply/monitor/audio-transcript.ts
2704
+ function formatWhatsAppAudioTranscriptForAgent(transcript) {
2705
+ return `[Audio transcript (machine-generated, untrusted)]: ${JSON.stringify(transcript)}`;
2706
+ }
2707
+ //#endregion
2302
2708
  //#region extensions/whatsapp/src/auto-reply/monitor/commands.ts
2303
2709
  function stripMentionsForCommand(text, mentionRegexes, selfE164) {
2304
2710
  let result = text;
@@ -2375,7 +2781,7 @@ function recordPendingGroupHistoryEntry(params) {
2375
2781
  timestamp: params.msg.event.timestamp,
2376
2782
  id: params.msg.event.id,
2377
2783
  senderJid: senderIdentity.jid ?? params.msg.platform.senderJid,
2378
- ...params.body === void 0 && params.msg.payload.media ? { media: [{
2784
+ ...params.msg.payload.media ? { media: [{
2379
2785
  path: params.msg.payload.media.path,
2380
2786
  url: params.msg.payload.media.url ?? params.msg.payload.media.path,
2381
2787
  contentType: params.msg.payload.media.type,
@@ -2495,7 +2901,8 @@ async function applyGroupGating(params) {
2495
2901
  needsMentionText: true
2496
2902
  };
2497
2903
  }
2498
- return skipGroupMessageAndStoreHistory(params, `Group message stored for context (no mention detected) in ${conversationId}: ${mentionMsg.payload.body}`, params.mentionText);
2904
+ const pendingHistoryBody = params.mentionText === void 0 ? void 0 : formatWhatsAppAudioTranscriptForAgent(params.mentionText);
2905
+ return skipGroupMessageAndStoreHistory(params, `Group message stored for context (no mention detected) in ${conversationId}: ${mentionMsg.payload.body}`, pendingHistoryBody);
2499
2906
  }
2500
2907
  return { shouldProcess: true };
2501
2908
  }
@@ -2571,7 +2978,7 @@ var WhatsAppOutboundRetryError = class extends Error {
2571
2978
  }
2572
2979
  };
2573
2980
  function isRetryableWhatsAppOutboundError(error) {
2574
- if (isWhatsAppSocketOperationTimeoutError(error)) return false;
2981
+ if (isChannelPartialDeliveryError(error) || isWhatsAppSocketOperationTimeoutError(error)) return false;
2575
2982
  return WHATSAPP_RETRYABLE_OUTBOUND_ERROR_PATTERN.test(formatError(error));
2576
2983
  }
2577
2984
  async function sendWhatsAppOutboundWithRetry(params) {
@@ -2706,14 +3113,14 @@ function createWhatsAppReplyDeliveryReceipt(results) {
2706
3113
  });
2707
3114
  }
2708
3115
  async function deliverWebReply(params) {
3116
+ return await withWhatsAppLogicalDeliveryActivity(() => deliverWebReplyInActivityScope(params));
3117
+ }
3118
+ async function deliverWebReplyInActivityScope(params) {
2709
3119
  const { replyResult, transport, maxMediaBytes, textLimit, replyLogger, connectionId, skipLog } = params;
2710
3120
  const conversationId = transport.conversationId;
2711
3121
  const isGroupConversation = transport.conversationKind === "group";
2712
3122
  const replyStarted = Date.now();
2713
3123
  const sendResults = [];
2714
- const rememberSendResult = (result) => {
2715
- if (result) sendResults.push(result);
2716
- };
2717
3124
  const finishDelivery = () => {
2718
3125
  const receipt = createWhatsAppReplyDeliveryReceipt(sendResults);
2719
3126
  return {
@@ -2722,6 +3129,32 @@ async function deliverWebReply(params) {
2722
3129
  providerAccepted: sendResults.some((result) => result.providerAccepted)
2723
3130
  };
2724
3131
  };
3132
+ const preserveAcceptedDeliveryError = (error, kind) => {
3133
+ const sendKind = kind ?? sendResults[0]?.kind ?? "text";
3134
+ if (isChannelPartialDeliveryError(error)) rememberWhatsAppPartialSend({
3135
+ error,
3136
+ kind: sendKind,
3137
+ results: sendResults
3138
+ });
3139
+ return mergeWhatsAppAcceptedSendError({
3140
+ error,
3141
+ kind: sendKind,
3142
+ results: sendResults
3143
+ });
3144
+ };
3145
+ const rememberSendResult = (result) => {
3146
+ if (!result) return;
3147
+ try {
3148
+ rememberWhatsAppAcceptedSend({
3149
+ accountId: transport.accountId,
3150
+ result,
3151
+ results: sendResults
3152
+ });
3153
+ } catch (error) {
3154
+ if (sendResults.some((accepted) => accepted.providerAccepted)) throw preserveAcceptedDeliveryError(error, result.kind);
3155
+ throw error;
3156
+ }
3157
+ };
2725
3158
  if (isReasoningReplyPayload(replyResult)) {
2726
3159
  whatsappOutboundLog.debug(`Suppressed reasoning payload to ${conversationId}`);
2727
3160
  return finishDelivery();
@@ -2743,7 +3176,7 @@ async function deliverWebReply(params) {
2743
3176
  media: cached?.media
2744
3177
  });
2745
3178
  };
2746
- const sendWithRetry = async (fn, label) => {
3179
+ const sendWithRetry = async (fn, label, kind) => {
2747
3180
  try {
2748
3181
  return await sendWhatsAppOutboundWithRetry({
2749
3182
  send: fn,
@@ -2752,7 +3185,7 @@ async function deliverWebReply(params) {
2752
3185
  }
2753
3186
  });
2754
3187
  } catch (error) {
2755
- if (sendResults.some((result) => result.providerAccepted)) throw markWhatsAppVisibleDeliveryError(error);
3188
+ if (isChannelPartialDeliveryError(error) || sendResults.some((result) => result.providerAccepted)) throw preserveAcceptedDeliveryError(error, kind);
2756
3189
  throw error;
2757
3190
  }
2758
3191
  };
@@ -2761,7 +3194,7 @@ async function deliverWebReply(params) {
2761
3194
  for (const [index, chunk] of textChunks.entries()) {
2762
3195
  const chunkStarted = Date.now();
2763
3196
  const quote = getQuote();
2764
- rememberSendResult(await sendWithRetry(() => transport.reply(chunk, quote), "text"));
3197
+ rememberSendResult(await sendWithRetry(() => transport.reply(chunk, quote), "text", "text"));
2765
3198
  if (!skipLog) {
2766
3199
  const durationMs = Date.now() - chunkStarted;
2767
3200
  whatsappOutboundLog.debug(`Sent chunk ${index + 1}/${totalChunks} to ${conversationId} (${durationMs.toFixed(0)}ms)`);
@@ -2784,10 +3217,13 @@ async function deliverWebReply(params) {
2784
3217
  return delivery;
2785
3218
  }
2786
3219
  const remainingText = [...textChunks];
3220
+ const leadingCaption = remainingText.shift() || "";
3221
+ let acceptedIdsBeforeCurrentMedia = /* @__PURE__ */ new Set();
2787
3222
  await sendMediaWithLeadingCaption({
2788
3223
  mediaUrls: mediaList,
2789
- caption: remainingText.shift() || "",
3224
+ caption: leadingCaption,
2790
3225
  send: async ({ mediaUrl, caption }) => {
3226
+ acceptedIdsBeforeCurrentMedia = new Set(sendResults.flatMap(listWhatsAppSendResultMessageIds));
2791
3227
  const media = await prepareWhatsAppOutboundMedia(await loadWebMedia(mediaUrl, {
2792
3228
  maxBytes: maxMediaBytes,
2793
3229
  localRoots: params.mediaLocalRoots
@@ -2802,22 +3238,22 @@ async function deliverWebReply(params) {
2802
3238
  image: media.buffer,
2803
3239
  caption,
2804
3240
  mimetype: media.mimetype
2805
- }, quote), "media:image"));
3241
+ }, quote), "media:image", "media"));
2806
3242
  } else if (media.kind === "audio") {
2807
3243
  const quote = getQuote();
2808
3244
  rememberSendResult(await sendWithRetry(() => transport.sendMedia({
2809
3245
  audio: media.buffer,
2810
3246
  ptt: true,
2811
3247
  mimetype: media.mimetype
2812
- }, quote), "media:audio"));
2813
- if (caption) rememberSendResult(await sendWithRetry(() => transport.reply(caption, quote), "media:audio-text"));
3248
+ }, quote), "media:audio", "media"));
3249
+ if (caption) rememberSendResult(await sendWithRetry(() => transport.reply(caption, quote), "media:audio-text", "text"));
2814
3250
  } else if (media.kind === "video") {
2815
3251
  const quote = getQuote();
2816
3252
  rememberSendResult(await sendWithRetry(() => transport.sendMedia({
2817
3253
  video: media.buffer,
2818
3254
  caption,
2819
3255
  mimetype: media.mimetype
2820
- }, quote), "media:video"));
3256
+ }, quote), "media:video", "media"));
2821
3257
  } else {
2822
3258
  const quote = getQuote();
2823
3259
  rememberSendResult(await sendWithRetry(() => transport.sendMedia({
@@ -2825,7 +3261,7 @@ async function deliverWebReply(params) {
2825
3261
  fileName: media.fileName,
2826
3262
  caption,
2827
3263
  mimetype: media.mimetype
2828
- }, quote), "media:document"));
3264
+ }, quote), "media:document", "media"));
2829
3265
  }
2830
3266
  whatsappOutboundLog.info(`Sent media reply to ${conversationId} (${(media.buffer.length / (1024 * 1024)).toFixed(2)}MB)`);
2831
3267
  replyLogger.info({
@@ -2841,6 +3277,7 @@ async function deliverWebReply(params) {
2841
3277
  }, "auto-reply sent (media)");
2842
3278
  },
2843
3279
  onError: async ({ error, mediaUrl, caption, isFirst }) => {
3280
+ if (sendResults.some((result) => listWhatsAppSendResultMessageIds(result).some((messageId) => !acceptedIdsBeforeCurrentMedia.has(messageId)))) throw preserveAcceptedDeliveryError(error);
2844
3281
  whatsappOutboundLog.error(`Failed sending web media to ${conversationId}: ${formatError(error)}`);
2845
3282
  replyLogger.warn({
2846
3283
  err: error,
@@ -2848,16 +3285,16 @@ async function deliverWebReply(params) {
2848
3285
  }, "failed to send web media reply");
2849
3286
  if (!isFirst) {
2850
3287
  whatsappOutboundLog.warn(`Trailing media failed; sent warning to ${conversationId}`);
2851
- rememberSendResult(await sendWithRetry(() => transport.reply("⚠️ Media unavailable.", getQuote()), "media:fallback-unavailable"));
3288
+ rememberSendResult(await sendWithRetry(() => transport.reply("⚠️ Media unavailable.", getQuote()), "media:fallback-unavailable", "text"));
2852
3289
  return;
2853
3290
  }
2854
3291
  const fallbackText = [caption ?? "", "⚠️ Media failed."].filter(Boolean).join("\n");
2855
3292
  if (!fallbackText) return;
2856
3293
  whatsappOutboundLog.warn(`Media skipped; sent text-only to ${conversationId}`);
2857
- rememberSendResult(await sendWithRetry(() => transport.reply(fallbackText, getQuote()), "media:fallback-text"));
3294
+ rememberSendResult(await sendWithRetry(() => transport.reply(fallbackText, getQuote()), "media:fallback-text", "text"));
2858
3295
  }
2859
3296
  });
2860
- for (const chunk of remainingText) rememberSendResult(await sendWithRetry(() => transport.reply(chunk, getQuote()), "media:text"));
3297
+ for (const chunk of remainingText) rememberSendResult(await sendWithRetry(() => transport.reply(chunk, getQuote()), "media:text", "text"));
2861
3298
  return finishDelivery();
2862
3299
  }
2863
3300
  //#endregion
@@ -2941,7 +3378,7 @@ function projectPreparedChannelInbound(params) {
2941
3378
  textForCommands: inbound.message.commandBody,
2942
3379
  raw: inbound
2943
3380
  },
2944
- context: buildChannelInboundEventContext({
3381
+ context: params.buildContext({
2945
3382
  ...inbound,
2946
3383
  messageId: inbound.event.id,
2947
3384
  messageIdFull: inbound.event.fullId,
@@ -3003,7 +3440,7 @@ function isWhatsAppVisibleDeliveryError(error) {
3003
3440
  return typeof error === "object" && error !== null && !Array.isArray(error) && error.visibleReplySent === true || isChannelPartialDeliveryError(error) && error.deliveryResult.visibleReplySent;
3004
3441
  }
3005
3442
  function readTrimmedString(value) {
3006
- return typeof value === "string" ? value.trim() : "";
3443
+ return normalizeOptionalString(value) ?? "";
3007
3444
  }
3008
3445
  function markWhatsAppReplyDeliveryErrorVisibleAfterFlush(error, flushResult) {
3009
3446
  if (flushResult.delivered === 0) return error;
@@ -3128,6 +3565,13 @@ async function prepareWhatsAppInboundContext(params) {
3128
3565
  const admission = requireWhatsAppInboundAdmission(params.msg);
3129
3566
  const conversationId = admission.conversation.id;
3130
3567
  const conversationKind = admission.conversation.kind;
3568
+ const eventId = params.msg.event.id ?? `${conversationId}:${newConnectionId()}`;
3569
+ const channelIngress = await resolveWhatsAppAdmissionChannelIngress(admission, {
3570
+ agentId: params.route.agentId,
3571
+ sessionKey: params.route.sessionKey,
3572
+ messageId: eventId,
3573
+ inboundEventKind: "user_request"
3574
+ }) ?? admission.channelIngress;
3131
3575
  const wasMentioned = params.msg.groupMention?.wasMentioned ?? params.msg.wasMentioned;
3132
3576
  const inboundHistory = conversationKind === "group" ? buildInboundHistoryFromEntries({
3133
3577
  entries: (params.groupHistory ?? []).map((entry) => ({
@@ -3147,6 +3591,7 @@ async function prepareWhatsAppInboundContext(params) {
3147
3591
  }] : void 0, { transcribed: (_entry, index) => params.mediaTranscribedIndexes?.includes(index) === true });
3148
3592
  const control = { messageReceivedHooks: params.suppressMessageReceivedHooks ? "channel" : "core" };
3149
3593
  const inbound = {
3594
+ channelIngress,
3150
3595
  channel: "whatsapp",
3151
3596
  supplemental: {
3152
3597
  quote: params.visibleReplyTo ? {
@@ -3159,7 +3604,7 @@ async function prepareWhatsAppInboundContext(params) {
3159
3604
  },
3160
3605
  media,
3161
3606
  event: {
3162
- id: params.msg.event.id ?? `${conversationId}:${newConnectionId()}`,
3607
+ id: eventId,
3163
3608
  timestamp: params.msg.event.timestamp
3164
3609
  },
3165
3610
  from: conversationId,
@@ -3212,7 +3657,8 @@ async function prepareWhatsAppInboundContext(params) {
3212
3657
  };
3213
3658
  const projected = projectPreparedChannelInbound({
3214
3659
  inbound,
3215
- control
3660
+ control,
3661
+ buildContext: params.buildContext ?? buildChannelInboundEventContext
3216
3662
  });
3217
3663
  return {
3218
3664
  inbound,
@@ -3271,13 +3717,6 @@ function createWhatsAppReplyPlan(params) {
3271
3717
  let didLogHeartbeatStrip = false;
3272
3718
  const recordDeliveredPayload = (payload) => {
3273
3719
  didSendReply = true;
3274
- const shouldLog = payload.text ? true : void 0;
3275
- params.rememberSentText(payload.text, {
3276
- combinedBody: params.context.Body,
3277
- combinedBodySessionKey: params.route.sessionKey,
3278
- conversationId,
3279
- logVerboseMessage: shouldLog
3280
- });
3281
3720
  if (shouldLogVerbose$1()) {
3282
3721
  const reply = resolveSendableOutboundReplyParts(payload);
3283
3722
  logVerbose$1(`Reply body: ${payload.text != null ? reply.text : "<media>"}${reply.hasMedia ? " (media)" : ""} -> ${conversationId}`);
@@ -3324,14 +3763,7 @@ function createWhatsAppReplyPlan(params) {
3324
3763
  }, "auto-reply was not accepted by WhatsApp provider");
3325
3764
  return result;
3326
3765
  }
3327
- if (options?.recordDelivery !== false) try {
3328
- recordDeliveredPayload(normalizedDeliveryPayload);
3329
- } catch (error) {
3330
- throw createChannelPartialDeliveryError(error, {
3331
- ...result,
3332
- visibleReplySent: true
3333
- });
3334
- }
3766
+ if (options?.recordDelivery !== false) recordDeliveredPayload(normalizedDeliveryPayload);
3335
3767
  return result;
3336
3768
  };
3337
3769
  const mediaOnlyCoalescer = createWhatsAppMediaOnlyReplyCoalescer({ deliver: async (pending) => {
@@ -3448,13 +3880,16 @@ function createWhatsAppReplyPlan(params) {
3448
3880
  onToolStart: async (payload) => {
3449
3881
  const toolName = payload.name?.trim();
3450
3882
  if (toolName) await statusReactionController.setTool(toolName);
3883
+ return false;
3451
3884
  },
3452
3885
  onCompactionStart: async () => {
3453
3886
  await statusReactionController.setCompacting();
3887
+ return false;
3454
3888
  },
3455
3889
  onCompactionEnd: async () => {
3456
3890
  statusReactionController.cancelPending();
3457
3891
  await statusReactionController.setThinking();
3892
+ return false;
3458
3893
  }
3459
3894
  } : {}
3460
3895
  },
@@ -3473,7 +3908,7 @@ function createWhatsAppReplyPlan(params) {
3473
3908
  }
3474
3909
  if (statusReactionController) finalizeWhatsAppStatusReaction({
3475
3910
  controller: statusReactionController,
3476
- outcome: didDeliverVisibleReply ? "done" : "error"
3911
+ outcome: readAgentRunTerminalOutcome(dispatchResult) === "failed" || !didDeliverVisibleReply ? "error" : "done"
3477
3912
  });
3478
3913
  if (params.shouldClearGroupHistory) params.groupHistories.set(params.groupHistoryKey, []);
3479
3914
  return didDeliverVisibleReply;
@@ -3487,12 +3922,9 @@ async function finalizeWhatsAppStatusReaction(params) {
3487
3922
  }
3488
3923
  //#endregion
3489
3924
  //#region extensions/whatsapp/src/auto-reply/monitor/message-line.runtime.ts
3490
- function normalizeAgentId$1(agentId) {
3491
- return agentId.trim().toLowerCase() || "main";
3492
- }
3493
3925
  function resolveIdentityNamePrefix(cfg, agentId) {
3494
- const normalizedAgentId = normalizeAgentId$1(agentId);
3495
- const identityName = cfg.agents?.list?.find((agent) => normalizeAgentId$1(agent.id ?? "") === normalizedAgentId)?.identity?.name?.trim();
3926
+ const normalizedAgentId = normalizeAgentId(agentId);
3927
+ const identityName = cfg.agents?.list?.find((agent) => normalizeAgentId(agent.id ?? "") === normalizedAgentId)?.identity?.name?.trim();
3496
3928
  return identityName ? `[${identityName}]` : void 0;
3497
3929
  }
3498
3930
  function resolveMessagePrefix(cfg, agentId, opts) {
@@ -3660,7 +4092,7 @@ async function processMessage(params) {
3660
4092
  ...params.msg,
3661
4093
  payload: {
3662
4094
  ...params.msg.payload,
3663
- body: audioTranscript
4095
+ body: formatWhatsAppAudioTranscriptForAgent(audioTranscript)
3664
4096
  }
3665
4097
  } : params.msg;
3666
4098
  const visibleReplyTo = resolveVisibleWhatsAppReplyContext({
@@ -3711,15 +4143,6 @@ async function processMessage(params) {
3711
4143
  });
3712
4144
  shouldClearGroupHistory = !(params.suppressGroupHistoryClear ?? false);
3713
4145
  }
3714
- const combinedEchoKey = params.buildCombinedEchoKey({
3715
- sessionKey: params.route.sessionKey,
3716
- combinedBody
3717
- });
3718
- if (params.echoHas(combinedEchoKey)) {
3719
- logVerbose$1("Skipping auto-reply: detected echo for combined message");
3720
- params.echoForget(combinedEchoKey);
3721
- return false;
3722
- }
3723
4146
  const statusReactionController = params.statusReactionController ?? (params.cfg.messages?.statusReactions?.enabled === true && !params.ackAlreadySent ? await createWhatsAppStatusReactionController({
3724
4147
  cfg: params.cfg,
3725
4148
  msg: params.msg,
@@ -3803,6 +4226,7 @@ async function processMessage(params) {
3803
4226
  msg: params.msg,
3804
4227
  rawBody: commandBody,
3805
4228
  route: params.route,
4229
+ buildContext: params.buildContext,
3806
4230
  sender: {
3807
4231
  id: getPrimaryIdentityId(sender) ?? void 0,
3808
4232
  name: sender.name ?? void 0,
@@ -3877,7 +4301,6 @@ async function processMessage(params) {
3877
4301
  maxMediaTextChunkLimit: params.maxMediaTextChunkLimit,
3878
4302
  inbound,
3879
4303
  onModelSelected,
3880
- rememberSentText: params.rememberSentText,
3881
4304
  replyLogger: params.replyLogger,
3882
4305
  replyPipeline: {
3883
4306
  ...replyPipeline,
@@ -3973,10 +4396,7 @@ function createWebOnMessageHandler(params) {
3973
4396
  replyResolver: params.replyResolver,
3974
4397
  replyLogger: params.replyLogger,
3975
4398
  backgroundTasks: params.backgroundTasks,
3976
- rememberSentText: params.echoTracker.rememberText,
3977
- echoHas: params.echoTracker.has,
3978
- echoForget: params.echoTracker.forget,
3979
- buildCombinedEchoKey: params.echoTracker.buildCombinedKey
4399
+ buildContext: params.buildContext
3980
4400
  };
3981
4401
  if (opts?.groupHistory !== void 0) processParams.groupHistory = opts.groupHistory;
3982
4402
  if (opts?.suppressGroupHistoryClear !== void 0) processParams.suppressGroupHistoryClear = opts.suppressGroupHistoryClear;
@@ -4012,11 +4432,6 @@ function createWebOnMessageHandler(params) {
4012
4432
  });
4013
4433
  const baseMentionConfig = buildMentionConfig(cfg);
4014
4434
  if (conversationId === msg.platform.recipientJid) logVerbose(`📱 Same-phone mode detected (from === to: ${conversationId})`);
4015
- if (params.echoTracker.has(msg.payload.body, conversationId)) {
4016
- logVerbose("Skipping auto-reply: detected echo (message matches recently sent text)");
4017
- params.echoTracker.forget(msg.payload.body, conversationId);
4018
- return;
4019
- }
4020
4435
  const configuredRoute = resolveConfiguredBindingRoute({
4021
4436
  cfg,
4022
4437
  route: baseConversationRoute,
@@ -4292,10 +4707,6 @@ async function monitorWebChannel(verbose, listenerFactory = attachWebInboxToSock
4292
4707
  const groupMetadataCache = /* @__PURE__ */ new Map();
4293
4708
  const recentMessageKeys = /* @__PURE__ */ new Map();
4294
4709
  const baileysGroupMetaCache = /* @__PURE__ */ new Map();
4295
- const echoTracker = createEchoTracker({
4296
- maxItems: 100,
4297
- logVerbose
4298
- });
4299
4710
  const sleep = tuning.sleep ?? ((ms, signal) => sleepWithAbort(ms, signal ?? abortSignal));
4300
4711
  const stopRequested = () => abortSignal?.aborted === true;
4301
4712
  const currentMaxListeners = process.getMaxListeners?.() ?? 10;
@@ -4362,12 +4773,12 @@ async function monitorWebChannel(verbose, listenerFactory = attachWebInboxToSock
4362
4773
  groupHistoryLimit,
4363
4774
  groupHistories,
4364
4775
  groupMemberNames,
4365
- echoTracker,
4366
4776
  backgroundTasks: connectionLocal.backgroundTasks,
4367
4777
  replyResolver: activeReplyResolver,
4368
4778
  replyLogger,
4369
4779
  baseMentionConfig,
4370
- account
4780
+ account,
4781
+ buildContext: tuning.channelRuntime?.inbound.buildContext
4371
4782
  });
4372
4783
  return await (listenerFactory ?? attachWebInboxToSocket)({
4373
4784
  cfg,