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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/{accounts-CSqLvCY5.js → accounts-BX2fZUyR.js} +1 -1
  2. package/dist/action-runtime-api.js +1 -1
  3. package/dist/{action-runtime-BzfDFPbH.js → action-runtime-pM-ExU0l.js} +2 -2
  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-CkMMvUjk.js} +4 -4
  8. package/dist/{approval-reactions-Cu66aYdq.js → approval-reactions-Jyr1JaAK.js} +29 -81
  9. package/dist/auth-presence.js +1 -1
  10. package/dist/{auth-store-DcEpIOmE.js → auth-store-CtpBqH5S.js} +82 -18
  11. package/dist/call-tool-api.js +1 -1
  12. package/dist/{channel-Dg62olhy.js → channel-DRMYRfZ8.js} +56 -160
  13. package/dist/channel-plugin-api.js +1 -1
  14. package/dist/{channel-react-action-J_YtyMGX.js → channel-react-action-CstrR0CT.js} +14 -12
  15. package/dist/{channel.runtime-BP0xNmLi.js → channel.runtime-X3JC2gaq.js} +4 -4
  16. package/dist/channel.setup-DhBBhbQv.js +14 -0
  17. package/dist/{connection-controller-BOB2MVKY.js → connection-controller-CIYuBJrs.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-P7_3U73Z.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-BkpKpndm.js} +4 -4
  26. package/dist/{login-qr-Bncpb6js.js → login-qr-1dpv6jjT.js} +4 -4
  27. package/dist/login-qr-runtime.js +1 -1
  28. package/dist/{monitor-CfkaUSL2.js → monitor-IzwQKizW.js} +545 -549
  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-D37ddk22.js → send-CwvTU4RI.js} +298 -23
  33. package/dist/{send-api-7wk6eQyW.js → send-api-obOSm2cC.js} +126 -137
  34. package/dist/setup-entry.js +1 -8
  35. package/dist/{setup-finalize-BZucx24W.js → setup-finalize-n4K2I2gJ.js} +4 -4
  36. package/dist/setup-plugin-api.js +1 -1
  37. package/dist/{setup-surface-BkVcd769.js → setup-surface-Cwn3zyxK.js} +3 -3
  38. package/dist/{shared-CAxd15bp.js → shared-wg42NKFB.js} +13 -8
  39. package/dist/{socket-close-DridEuzz.js → socket-close-2T0_jZTN.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,62 +1,248 @@
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 { c as normalizeWhatsAppPayloadTextPreservingIndentation, d as listWhatsAppSendResultMessageIds, f as mergeWhatsAppAcceptedSendError, g as withWhatsAppLogicalDeliveryActivity, h as rememberWhatsAppPartialSend, l as prepareWhatsAppOutboundMedia, m as rememberWhatsAppAcceptedSend, o as normalizeWhatsAppOutboundPayload, p as normalizeWhatsAppSendResult, r as sendReactionWhatsApp, v as resolveWhatsAppReactionLevel } from "./send-CwvTU4RI.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-CtpBqH5S.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-2T0_jZTN.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 { _ as resolveInboundMediaMimetype, a as addWhatsAppImagePreviewFields, c as extractContextInfo, d as extractMediaKind, f as extractMentionedJids, g as projectWhatsAppInboundMessage, 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-obOSm2cC.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-CIYuBJrs.js";
16
+ import { n as maybeResolveWhatsAppApprovalReaction } from "./approval-reactions-Jyr1JaAK.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";
59
+ import { formatAudioTranscriptForAgent } from "openclaw/plugin-sdk/media-understanding-runtime";
54
60
  import { hasControlCommand, isControlCommandMessage, shouldComputeCommandAuthorized } from "openclaw/plugin-sdk/command-detection";
55
61
  import { createInternalHookEvent, deriveInboundMessageHookContext, fireAndForgetBoundedHook, toInternalMessageReceivedContext, toPluginMessageContext, toPluginMessageReceivedEvent, triggerInternalHook } from "openclaw/plugin-sdk/hook-runtime";
56
62
  import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
57
63
  import { resolveBatchedReplyThreadingPolicy } from "openclaw/plugin-sdk/reply-reference";
58
64
  import { LocalMediaAccessError, getDefaultLocalRoots, loadWebMedia, loadWebMediaRaw, optimizeImageToJpeg, optimizeImageToPng } from "openclaw/plugin-sdk/web-media";
59
65
  import { createChannelApiRetryRunner } from "openclaw/plugin-sdk/retry-runtime";
66
+ import { resolveAgentConfig } from "openclaw/plugin-sdk/agent-scope-runtime";
67
+ //#region extensions/whatsapp/src/inbound/group-conversation.ts
68
+ function resolveWhatsAppGroupConversationId(conversationId) {
69
+ return resolveGroupSessionKey({
70
+ From: conversationId,
71
+ ChatType: "group",
72
+ Provider: "whatsapp"
73
+ })?.id ?? conversationId;
74
+ }
75
+ //#endregion
76
+ //#region extensions/whatsapp/src/inbound/admission.ts
77
+ const ingressResolverByAdmission = /* @__PURE__ */ new WeakMap();
78
+ function copyAccount(account) {
79
+ const copied = {
80
+ accountId: account.accountId,
81
+ enabled: account.enabled,
82
+ sendReadReceipts: account.sendReadReceipts
83
+ };
84
+ if (account.name) copied.name = account.name;
85
+ if (typeof account.selfChatMode === "boolean") copied.selfChatMode = account.selfChatMode;
86
+ if (account.replyToMode) copied.replyToMode = account.replyToMode;
87
+ return copied;
88
+ }
89
+ function buildWhatsAppInboundAdmission(params) {
90
+ const admission = {
91
+ channelIngress: params.channelIngress,
92
+ accountId: params.policy.account.accountId,
93
+ isSelfChat: params.policy.isSelfChat,
94
+ account: copyAccount(params.policy.account),
95
+ conversation: {
96
+ kind: params.isGroup ? "group" : "direct",
97
+ id: params.conversationId,
98
+ groupSessionId: resolveWhatsAppGroupConversationId(params.conversationId)
99
+ },
100
+ sender: {
101
+ id: params.senderId,
102
+ isSamePhone: params.policy.isSamePhone(params.senderId)
103
+ },
104
+ ingress: {
105
+ admission: params.access.ingress.admission,
106
+ decision: params.access.ingress.decision,
107
+ decisiveGateId: params.access.ingress.decisiveGateId,
108
+ reasonCode: params.access.ingress.reasonCode
109
+ },
110
+ senderAccess: {
111
+ allowed: params.access.senderAccess.allowed,
112
+ decision: params.access.senderAccess.decision,
113
+ reasonCode: params.access.senderAccess.reasonCode,
114
+ providerMissingFallbackApplied: params.access.senderAccess.providerMissingFallbackApplied
115
+ },
116
+ commandAccess: {
117
+ requested: params.access.commandAccess.requested,
118
+ authorized: params.access.commandAccess.authorized,
119
+ shouldBlockControlCommand: params.access.commandAccess.shouldBlockControlCommand,
120
+ reasonCode: params.access.commandAccess.reasonCode
121
+ },
122
+ activationAccess: {
123
+ ran: params.access.activationAccess.ran,
124
+ allowed: params.access.activationAccess.allowed,
125
+ shouldSkip: params.access.activationAccess.shouldSkip,
126
+ reasonCode: params.access.activationAccess.reasonCode
127
+ }
128
+ };
129
+ if (params.resolveChannelIngress) ingressResolverByAdmission.set(admission, params.resolveChannelIngress);
130
+ return admission;
131
+ }
132
+ async function resolveWhatsAppAdmissionChannelIngress(admission, contextBinding) {
133
+ return await ingressResolverByAdmission.get(admission)?.(contextBinding);
134
+ }
135
+ function requireWhatsAppInboundAdmission(params) {
136
+ if (!params.admission) throw new Error("WhatsApp inbound message is missing admission facts");
137
+ return params.admission;
138
+ }
139
+ //#endregion
140
+ //#region extensions/whatsapp/src/inbound-policy.ts
141
+ function normalizeWhatsAppIngressPhone(value) {
142
+ const trimmed = value.trim();
143
+ if (!trimmed) return null;
144
+ return normalizeE164(trimmed);
145
+ }
146
+ function buildResolvedWhatsAppGroupConfig(params) {
147
+ return { channels: { whatsapp: {
148
+ groupPolicy: params.groupPolicy,
149
+ groups: params.groups
150
+ } } };
151
+ }
152
+ function resolveWhatsAppInboundPolicy(params) {
153
+ const account = resolveWhatsAppAccount({
154
+ cfg: params.cfg,
155
+ accountId: params.accountId
156
+ });
157
+ const configuredAllowFrom = account.allowFrom ?? [];
158
+ const dmPolicy = account.dmPolicy ?? "pairing";
159
+ const dmAllowFrom = configuredAllowFrom.length > 0 ? configuredAllowFrom : params.selfE164 ? [params.selfE164] : [];
160
+ const groupAllowFrom = (Array.isArray(account.groupAllowFrom) && account.groupAllowFrom.length > 0 ? account.groupAllowFrom : void 0) ?? (configuredAllowFrom.length > 0 ? configuredAllowFrom : void 0) ?? [];
161
+ const defaultGroupPolicy = resolveDefaultGroupPolicy(params.cfg);
162
+ const { groupPolicy, providerMissingFallbackApplied } = resolveWhatsAppRuntimeGroupPolicy({
163
+ providerConfigPresent: params.cfg.channels?.whatsapp !== void 0,
164
+ groupPolicy: account.groupPolicy,
165
+ defaultGroupPolicy
166
+ });
167
+ const resolvedGroupCfg = buildResolvedWhatsAppGroupConfig({
168
+ groupPolicy,
169
+ groups: account.groups
170
+ });
171
+ const isSamePhone = (value) => typeof value === "string" && typeof params.selfE164 === "string" && value === params.selfE164;
172
+ return {
173
+ account,
174
+ dmPolicy,
175
+ groupPolicy,
176
+ configuredAllowFrom,
177
+ dmAllowFrom,
178
+ groupAllowFrom,
179
+ isSelfChat: account.selfChatMode ?? isSelfChatMode(params.selfE164, configuredAllowFrom),
180
+ providerMissingFallbackApplied,
181
+ isSamePhone,
182
+ resolveConversationGroupPolicy: (conversationId) => resolveChannelGroupPolicy({
183
+ cfg: resolvedGroupCfg,
184
+ channel: "whatsapp",
185
+ groupId: resolveWhatsAppGroupConversationId(conversationId),
186
+ hasGroupAllowFrom: groupAllowFrom.length > 0
187
+ }),
188
+ resolveConversationRequireMention: (conversationId) => resolveChannelGroupRequireMention({
189
+ cfg: resolvedGroupCfg,
190
+ channel: "whatsapp",
191
+ groupId: resolveWhatsAppGroupConversationId(conversationId)
192
+ })
193
+ };
194
+ }
195
+ async function resolveWhatsAppIngressAccess(params) {
196
+ return await resolveStableChannelMessageIngress({
197
+ channelId: "whatsapp",
198
+ accountId: params.policy.account.accountId,
199
+ identity: {
200
+ key: "whatsapp-sender-phone",
201
+ kind: "phone",
202
+ normalize: normalizeWhatsAppIngressPhone,
203
+ sensitivity: "pii",
204
+ entryIdPrefix: "whatsapp-entry"
205
+ },
206
+ cfg: params.cfg,
207
+ useDefaultPairingStore: true,
208
+ subject: { stableId: params.senderId ?? "" },
209
+ conversation: {
210
+ kind: params.isGroup ? "group" : "direct",
211
+ id: params.conversationId
212
+ },
213
+ contextBinding: params.contextBinding,
214
+ dmPolicy: params.policy.dmPolicy,
215
+ groupPolicy: params.policy.groupPolicy,
216
+ policy: { groupAllowFromFallbackToAllowFrom: false },
217
+ providerMissingFallbackApplied: params.policy.providerMissingFallbackApplied,
218
+ allowFrom: !params.isGroup && params.policy.account.selfChatMode !== false && params.senderId && params.policy.isSamePhone(params.senderId) ? [...params.policy.dmAllowFrom, params.senderId] : params.policy.dmAllowFrom,
219
+ groupAllowFrom: params.policy.groupAllowFrom,
220
+ command: params.includeCommand === true ? {} : void 0
221
+ });
222
+ }
223
+ async function resolveWhatsAppCommandAuthorized(params) {
224
+ const self = getSelfIdentity(params.msg, params.authDir);
225
+ const admission = requireWhatsAppInboundAdmission(params.msg);
226
+ const policy = params.policy ?? resolveWhatsAppInboundPolicy({
227
+ cfg: params.cfg,
228
+ accountId: admission.accountId,
229
+ selfE164: self.e164 ?? null
230
+ });
231
+ const isGroup = admission.conversation.kind === "group";
232
+ const sender = getSenderIdentity(params.msg, params.authDir);
233
+ const dmSender = sender.e164 ?? admission.conversation.id;
234
+ const groupSender = sender.e164 ?? "";
235
+ if (!normalizeE164(isGroup ? groupSender : dmSender)) return false;
236
+ return (await resolveWhatsAppIngressAccess({
237
+ cfg: params.cfg,
238
+ policy,
239
+ isGroup,
240
+ conversationId: admission.conversation.id,
241
+ senderId: isGroup ? groupSender : dmSender,
242
+ includeCommand: true
243
+ })).commandAccess.authorized;
244
+ }
245
+ //#endregion
60
246
  //#region extensions/whatsapp/src/inbound/baileys-cache.ts
61
247
  const WHATSAPP_BAILEYS_CACHE_MAX_ENTRIES = 500;
62
248
  function rememberWhatsAppBaileysCacheEntry(cache, key, value, ttlMs) {
@@ -84,353 +270,6 @@ function readWhatsAppBaileysCacheEntry(cache, key) {
84
270
  return entry.value;
85
271
  }
86
272
  //#endregion
87
- //#region extensions/whatsapp/src/inbound/message-aliases.ts
88
- function normalizeQuoteSender(sender) {
89
- if (!sender?.displayName && !sender?.jid && !sender?.e164) return;
90
- return sender;
91
- }
92
- function buildQuoteFromFlatAliases(msg) {
93
- if (msg.replyTo) return {
94
- context: msg.replyTo,
95
- id: msg.replyTo.id,
96
- body: msg.replyTo.body,
97
- sender: normalizeQuoteSender({
98
- displayName: msg.replyTo.sender?.label ?? msg.replyToSender,
99
- jid: msg.replyTo.sender?.jid ?? msg.replyToSenderJid,
100
- e164: msg.replyTo.sender?.e164 ?? msg.replyToSenderE164
101
- })
102
- };
103
- if (!msg.replyToId && !msg.replyToBody && !msg.replyToSender && !msg.replyToSenderJid && !msg.replyToSenderE164) return;
104
- return {
105
- id: msg.replyToId,
106
- body: msg.replyToBody,
107
- sender: normalizeQuoteSender({
108
- displayName: msg.replyToSender,
109
- jid: msg.replyToSenderJid,
110
- e164: msg.replyToSenderE164
111
- })
112
- };
113
- }
114
- function buildGroupFromFlatAliases(msg) {
115
- const mentionJids = msg.mentions ?? msg.mentionedJids;
116
- if (!msg.groupSubject && !msg.groupParticipants?.length && !mentionJids?.length) return;
117
- return {
118
- subject: msg.groupSubject,
119
- participants: msg.groupParticipants,
120
- mentions: mentionJids?.length ? { jids: mentionJids } : void 0
121
- };
122
- }
123
- function ensureQuote(msg) {
124
- return msg.quote ??= {};
125
- }
126
- function ensureQuoteSender(msg) {
127
- const quote = ensureQuote(msg);
128
- return quote.sender ??= {};
129
- }
130
- function ensureGroup(msg) {
131
- return msg.group ??= {};
132
- }
133
- function ensureGroupMentions(msg) {
134
- const group = ensureGroup(msg);
135
- return group.mentions ??= {};
136
- }
137
- function ensureMedia(msg) {
138
- return msg.payload.media ??= {};
139
- }
140
- function setMediaField(msg, key, value) {
141
- if (value === void 0 && !msg.payload.media) return;
142
- ensureMedia(msg)[key] = value;
143
- }
144
- function defineDeprecatedAccessors(msg, descriptors) {
145
- Object.defineProperties(msg, Object.fromEntries(Object.entries(descriptors).map(([key, descriptor]) => [key, {
146
- configurable: true,
147
- enumerable: true,
148
- get: descriptor.get,
149
- set: descriptor.set
150
- }])));
151
- return msg;
152
- }
153
- function defineDeprecatedAliasAccessors(msg, descriptors) {
154
- defineDeprecatedAccessors(msg, descriptors);
155
- return msg;
156
- }
157
- function defineDeprecatedStructuredContextPayloadAlias(msg) {
158
- const channelStructuredContext = msg.payload.channelStructuredContext ?? msg.payload.untrustedStructuredContext;
159
- if (channelStructuredContext !== void 0) msg.payload.channelStructuredContext = channelStructuredContext;
160
- Object.defineProperty(msg.payload, "untrustedStructuredContext", {
161
- configurable: true,
162
- enumerable: true,
163
- get: () => msg.payload.channelStructuredContext,
164
- set: (value) => {
165
- msg.payload.channelStructuredContext = value;
166
- }
167
- });
168
- }
169
- function defineDeprecatedAdmissionTopLevelAccessors(msg) {
170
- let fallbackConversationId = msg.conversationId || msg.from;
171
- let fallbackAccountId = msg.accountId;
172
- let fallbackAccessControlPassed = msg.accessControlPassed;
173
- let fallbackChatType = msg.chatType;
174
- const conversationId = () => msg.admission?.conversation.id ?? fallbackConversationId;
175
- const setConversationId = (value) => {
176
- const next = value;
177
- fallbackConversationId = next;
178
- if (msg.admission) {
179
- msg.admission.conversation.id = next;
180
- msg.admission.conversation.groupSessionId = resolveWhatsAppGroupConversationId(next);
181
- }
182
- };
183
- return defineDeprecatedAccessors(msg, {
184
- from: {
185
- get: conversationId,
186
- set: setConversationId
187
- },
188
- conversationId: {
189
- get: conversationId,
190
- set: setConversationId
191
- },
192
- accountId: {
193
- get: () => msg.admission?.accountId ?? fallbackAccountId,
194
- set: (value) => {
195
- const next = value;
196
- fallbackAccountId = next;
197
- if (msg.admission) {
198
- msg.admission.accountId = next;
199
- msg.admission.account.accountId = next;
200
- }
201
- }
202
- },
203
- accessControlPassed: {
204
- get: () => {
205
- if (msg.admission?.ingress.decisiveGateId === "legacy-flat-compat") return fallbackAccessControlPassed;
206
- return msg.admission ? msg.admission.ingress.decision === "allow" : fallbackAccessControlPassed;
207
- },
208
- set: (value) => {
209
- if (!msg.admission) fallbackAccessControlPassed = value;
210
- }
211
- },
212
- chatType: {
213
- get: () => msg.admission?.conversation.kind ?? fallbackChatType,
214
- set: (value) => {
215
- const next = value;
216
- fallbackChatType = next;
217
- if (msg.admission) msg.admission.conversation.kind = next;
218
- }
219
- }
220
- });
221
- }
222
- function withDeprecatedWebInboundMessageFlatAliases(msg) {
223
- defineDeprecatedStructuredContextPayloadAlias(msg);
224
- return defineDeprecatedAliasAccessors(defineDeprecatedAdmissionTopLevelAccessors(msg), {
225
- id: {
226
- get: () => msg.event.id,
227
- set: (value) => msg.event.id = value
228
- },
229
- to: {
230
- get: () => msg.platform.recipientJid,
231
- set: (value) => msg.platform.recipientJid = value
232
- },
233
- body: {
234
- get: () => msg.payload.body,
235
- set: (value) => msg.payload.body = value
236
- },
237
- pushName: {
238
- get: () => msg.platform.pushName,
239
- set: (value) => msg.platform.pushName = value
240
- },
241
- timestamp: {
242
- get: () => msg.event.timestamp,
243
- set: (value) => msg.event.timestamp = value
244
- },
245
- chatId: {
246
- get: () => msg.platform.chatJid,
247
- set: (value) => msg.platform.chatJid = value
248
- },
249
- sender: {
250
- get: () => msg.platform.sender,
251
- set: (value) => msg.platform.sender = value
252
- },
253
- senderJid: {
254
- get: () => msg.platform.senderJid,
255
- set: (value) => msg.platform.senderJid = value
256
- },
257
- senderE164: {
258
- get: () => msg.platform.senderE164,
259
- set: (value) => msg.platform.senderE164 = value
260
- },
261
- senderName: {
262
- get: () => msg.platform.senderName,
263
- set: (value) => msg.platform.senderName = value
264
- },
265
- replyTo: {
266
- get: () => msg.quote?.context,
267
- set: (value) => ensureQuote(msg).context = value
268
- },
269
- replyToId: {
270
- get: () => msg.quote?.id ?? msg.quote?.context?.id,
271
- set: (value) => ensureQuote(msg).id = value
272
- },
273
- replyToBody: {
274
- get: () => msg.quote?.body ?? msg.quote?.context?.body,
275
- set: (value) => ensureQuote(msg).body = value
276
- },
277
- replyToSender: {
278
- get: () => msg.quote?.context?.sender?.label ?? msg.quote?.sender?.displayName,
279
- set: (value) => {
280
- const sender = ensureQuoteSender(msg);
281
- sender.displayName = value;
282
- if (msg.quote?.context?.sender) msg.quote.context.sender.label = value;
283
- }
284
- },
285
- replyToSenderJid: {
286
- get: () => msg.quote?.context?.sender?.jid ?? msg.quote?.sender?.jid,
287
- set: (value) => {
288
- const jid = value;
289
- ensureQuoteSender(msg).jid = jid;
290
- if (msg.quote?.context?.sender) msg.quote.context.sender.jid = jid;
291
- }
292
- },
293
- replyToSenderE164: {
294
- get: () => msg.quote?.context?.sender?.e164 ?? msg.quote?.sender?.e164,
295
- set: (value) => {
296
- const e164 = value;
297
- ensureQuoteSender(msg).e164 = e164;
298
- if (msg.quote?.context?.sender) msg.quote.context.sender.e164 = e164;
299
- }
300
- },
301
- groupSubject: {
302
- get: () => msg.group?.subject,
303
- set: (value) => ensureGroup(msg).subject = value
304
- },
305
- groupParticipants: {
306
- get: () => msg.group?.participants,
307
- set: (value) => ensureGroup(msg).participants = value
308
- },
309
- mentions: {
310
- get: () => msg.group?.mentions?.jids,
311
- set: (value) => ensureGroupMentions(msg).jids = value
312
- },
313
- mentionedJids: {
314
- get: () => msg.group?.mentions?.jids,
315
- set: (value) => ensureGroupMentions(msg).jids = value
316
- },
317
- self: {
318
- get: () => msg.platform.self,
319
- set: (value) => msg.platform.self = value
320
- },
321
- selfJid: {
322
- get: () => msg.platform.selfJid,
323
- set: (value) => msg.platform.selfJid = value
324
- },
325
- selfLid: {
326
- get: () => msg.platform.selfLid,
327
- set: (value) => msg.platform.selfLid = value
328
- },
329
- selfE164: {
330
- get: () => msg.platform.selfE164,
331
- set: (value) => msg.platform.selfE164 = value
332
- },
333
- fromMe: {
334
- get: () => msg.platform.fromMe,
335
- set: (value) => msg.platform.fromMe = value
336
- },
337
- location: {
338
- get: () => msg.payload.location,
339
- set: (value) => msg.payload.location = value
340
- },
341
- sendComposing: {
342
- get: () => msg.platform.sendComposing,
343
- set: (value) => msg.platform.sendComposing = value
344
- },
345
- reply: {
346
- get: () => msg.platform.reply,
347
- set: (value) => msg.platform.reply = value
348
- },
349
- sendMedia: {
350
- get: () => msg.platform.sendMedia,
351
- set: (value) => msg.platform.sendMedia = value
352
- },
353
- mediaPath: {
354
- get: () => msg.payload.media?.path,
355
- set: (value) => setMediaField(msg, "path", value)
356
- },
357
- mediaType: {
358
- get: () => msg.payload.media?.type,
359
- set: (value) => setMediaField(msg, "type", value)
360
- },
361
- mediaFileName: {
362
- get: () => msg.payload.media?.fileName,
363
- set: (value) => setMediaField(msg, "fileName", value)
364
- },
365
- mediaUrl: {
366
- get: () => msg.payload.media?.url,
367
- set: (value) => setMediaField(msg, "url", value)
368
- },
369
- channelStructuredContext: {
370
- get: () => msg.payload.channelStructuredContext,
371
- set: (value) => msg.payload.channelStructuredContext = value
372
- },
373
- untrustedStructuredContext: {
374
- get: () => msg.payload.channelStructuredContext,
375
- set: (value) => msg.payload.channelStructuredContext = value
376
- },
377
- isBatched: {
378
- get: () => msg.event.isBatched,
379
- set: (value) => msg.event.isBatched = value
380
- }
381
- });
382
- }
383
- function normalizeLegacyFlatWebInboundMessage(msg) {
384
- const media = msg.mediaPath || msg.mediaType || msg.mediaFileName || msg.mediaUrl ? {
385
- path: msg.mediaPath,
386
- type: msg.mediaType,
387
- fileName: msg.mediaFileName,
388
- url: msg.mediaUrl
389
- } : void 0;
390
- return withDeprecatedWebInboundMessageFlatAliases({
391
- ...msg,
392
- admission: msg.admission ?? buildDeprecatedFlatWhatsAppInboundAdmission(msg),
393
- event: {
394
- id: msg.id,
395
- timestamp: msg.timestamp,
396
- isBatched: msg.isBatched
397
- },
398
- payload: {
399
- body: msg.body,
400
- media,
401
- location: msg.location,
402
- channelStructuredContext: msg.channelStructuredContext ?? msg.untrustedStructuredContext
403
- },
404
- platform: {
405
- chatJid: msg.chatId,
406
- recipientJid: msg.to,
407
- sender: msg.sender,
408
- senderJid: msg.senderJid,
409
- senderE164: msg.senderE164,
410
- senderName: msg.senderName,
411
- pushName: msg.pushName,
412
- self: msg.self,
413
- selfJid: msg.selfJid,
414
- selfLid: msg.selfLid,
415
- selfE164: msg.selfE164,
416
- fromMe: msg.fromMe,
417
- sendComposing: msg.sendComposing,
418
- reply: msg.reply,
419
- sendMedia: msg.sendMedia
420
- },
421
- quote: buildQuoteFromFlatAliases(msg),
422
- group: buildGroupFromFlatAliases(msg)
423
- });
424
- }
425
- function normalizeWebInboundMessage(msg) {
426
- if (msg.event && msg.payload && msg.platform) return withDeprecatedWebInboundMessageFlatAliases(msg);
427
- if (msg.event || msg.payload || msg.platform || msg.quote || msg.group) throw new Error("WhatsApp inbound messages must be either legacy flat or canonical nested; partial nested contexts are not supported.");
428
- return normalizeLegacyFlatWebInboundMessage(msg);
429
- }
430
- function normalizeAdmittedWebInboundMessage(msg) {
431
- return requireAdmittedWhatsAppInboundMessage(normalizeWebInboundMessage(msg));
432
- }
433
- //#endregion
434
273
  //#region extensions/whatsapp/src/inbound/durable-payload.ts
435
274
  var WhatsAppIngressPermanentError = class extends Error {
436
275
  constructor(reason, message, options) {
@@ -625,10 +464,14 @@ function createWhatsAppGroupMetadataCacheOwner(params) {
625
464
  const cached = readGroupMetadataCacheEntry(localCache, jid);
626
465
  if (cached) return cached;
627
466
  try {
628
- const meta = await (params.getCurrentSock() ?? params.sock).groupMetadata(jid);
629
- rememberWhatsAppBaileysCacheEntry(params.baileysCache, jid, meta, GROUP_META_TTL_MS);
467
+ const hydratedEntry = params.baileysCache?.get(jid);
468
+ const providerMetadata = params.baileysCache ? readWhatsAppBaileysCacheEntry(params.baileysCache, jid) : void 0;
469
+ const hydratedMetadata = providerMetadata?.participants?.length ? providerMetadata : void 0;
470
+ const meta = hydratedMetadata ?? await (params.getCurrentSock() ?? params.sock).groupMetadata(jid);
471
+ if (!hydratedMetadata) rememberWhatsAppBaileysCacheEntry(params.baileysCache, jid, meta, GROUP_META_TTL_MS);
630
472
  publishedJids.add(jid);
631
473
  const entry = await summarize(meta);
474
+ if (hydratedMetadata && hydratedEntry) entry.expires = hydratedEntry.expiresAt;
632
475
  rememberGroupMetadataCacheEntry(reconnectCache, jid, {
633
476
  subject: entry.subject,
634
477
  expires: entry.expires
@@ -647,7 +490,7 @@ function createWhatsAppGroupMetadataCacheOwner(params) {
647
490
  }
648
491
  };
649
492
  const resolveOutboundMentions = async (jid, text) => {
650
- if (isJidGroup(jid) !== true || !mayContainWhatsAppOutboundMention(text)) return {
493
+ if (isJidGroup$1(jid) !== true || !mayContainWhatsAppOutboundMention(text)) return {
651
494
  text,
652
495
  mentionedJids: []
653
496
  };
@@ -819,7 +662,7 @@ function createWhatsAppInboundMessageDebouncer(options) {
819
662
  ...last.group,
820
663
  mentions: combinedMentions
821
664
  } : void 0;
822
- const combinedMessage = attachWhatsAppIngressLifecycle(withDeprecatedWebInboundMessageFlatAliases({
665
+ const combinedMessage = attachWhatsAppIngressLifecycle({
823
666
  ...last,
824
667
  turnAdoptionLifecycle: admissionLifecycle,
825
668
  payload: {
@@ -832,7 +675,7 @@ function createWhatsAppInboundMessageDebouncer(options) {
832
675
  ...last.event,
833
676
  isBatched: true
834
677
  }
835
- }), admissionLifecycle);
678
+ }, admissionLifecycle);
836
679
  await options.onMessage(combinedMessage);
837
680
  await settle();
838
681
  await Promise.all(orderedEntries.map((entry) => options.markRead(entry.readReceipt)));
@@ -898,27 +741,21 @@ function createWhatsAppInboundMessageDebouncer(options) {
898
741
  }
899
742
  //#endregion
900
743
  //#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
744
  function unwrapMessage(message) {
908
- return normalizeMessageContent(message);
745
+ return normalizeMessageContent$1(message);
909
746
  }
910
- async function downloadInboundMedia(msg, sock, maxBytes = 50 * 1024 * 1024) {
911
- const message = unwrapMessage(msg.message);
747
+ async function downloadInboundMedia(msg, sock, maxBytes = 50 * 1024 * 1024, normalizedMessage) {
748
+ const message = normalizedMessage ?? unwrapMessage(msg.message);
912
749
  if (!message) return;
913
750
  const mimetype = resolveInboundMediaMimetype(message);
914
751
  const fileName = message.documentMessage?.fileName ?? void 0;
915
- if (!message.imageMessage && !message.videoMessage && !message.documentMessage && !message.audioMessage && !message.stickerMessage) return;
752
+ if (!message.imageMessage && !message.videoMessage && !message.ptvMessage && !message.documentMessage && !message.audioMessage && !message.stickerMessage) return;
916
753
  return {
917
754
  saved: await saveMediaStream(await downloadMediaMessage(msg, "stream", {}, {
918
755
  reuploadRequest: sock.updateMediaMessage,
919
756
  logger: sock.logger
920
757
  }), mimetype, "inbound", maxBytes, fileName).catch((err) => {
921
- if (err instanceof Error && /Media exceeds/i.test(err.message)) throw new WhatsAppInboundMediaLimitExceededError(maxBytes);
758
+ if (err instanceof Error && /Media exceeds/i.test(err.message)) throw new Error(`Media exceeds ${Math.round(maxBytes / (1024 * 1024))}MB limit`);
922
759
  throw err;
923
760
  }),
924
761
  mimetype,
@@ -929,12 +766,18 @@ async function downloadQuotedInboundMedia(msg, sock, maxBytes = 50 * 1024 * 1024
929
766
  const contextInfo = extractContextInfo(unwrapMessage(msg.message));
930
767
  if (!contextInfo?.quotedMessage) return;
931
768
  const quotedMessage = contextInfo.quotedMessage;
769
+ const self = sock.user;
770
+ const quotedFromMe = identitiesOverlap({ jid: contextInfo.participant }, {
771
+ jid: self?.id,
772
+ lid: self?.lid,
773
+ e164: self?.phoneNumber
774
+ });
932
775
  return downloadInboundMedia({
933
776
  key: {
934
777
  id: contextInfo?.stanzaId || void 0,
935
778
  remoteJid: contextInfo.remoteJid ?? msg.key?.remoteJid ?? void 0,
936
779
  participant: contextInfo?.participant ?? void 0,
937
- fromMe: false
780
+ fromMe: quotedFromMe
938
781
  },
939
782
  message: quotedMessage,
940
783
  messageTimestamp: msg.messageTimestamp
@@ -942,19 +785,46 @@ async function downloadQuotedInboundMedia(msg, sock, maxBytes = 50 * 1024 * 1024
942
785
  }
943
786
  //#endregion
944
787
  //#region extensions/whatsapp/src/inbound/message-enrichment.ts
788
+ const inboundMediaLogger = createSubsystemLogger("gateway/channels/whatsapp").child("inbound");
789
+ const MAX_MEDIA_ERROR_MESSAGE_CHARS = 256;
790
+ function sanitizeMediaErrorMessage(error) {
791
+ 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);
792
+ }
793
+ function logMediaMaterializationFailure(params) {
794
+ const errorClass = params.error instanceof Error && /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/u.test(params.error.name) ? params.error.name : "Error";
795
+ const statusCode = getStatusCode(params.error);
796
+ const errorMessage = sanitizeMediaErrorMessage(params.error);
797
+ const failureDetails = [
798
+ `stage=${params.failureStage}`,
799
+ ...params.mediaKind ? [`kind=${params.mediaKind}`] : [],
800
+ ...typeof statusCode === "number" ? [`status=${statusCode}`] : []
801
+ ].join(" ");
802
+ inboundMediaLogger.warn("WhatsApp inbound media materialization failed", {
803
+ channel: "whatsapp",
804
+ messageId: params.messageId ?? void 0,
805
+ mediaKind: params.mediaKind,
806
+ mimeType: params.mimeType,
807
+ failureStage: params.failureStage,
808
+ errorClass,
809
+ ...typeof statusCode === "number" ? { statusCode } : {},
810
+ errorMessage,
811
+ consoleMessage: `WhatsApp inbound media materialization failed (${failureDetails}): ${errorMessage}`
812
+ });
813
+ }
945
814
  async function enrichWhatsAppInboundMessage(params) {
946
815
  const { msg, sock } = params;
947
- const location = extractLocationData(msg.message ?? void 0);
816
+ const messageProjection = projectWhatsAppInboundMessage(msg.message ?? void 0);
817
+ const location = extractLocationData(messageProjection);
948
818
  const locationText = location ? formatLocationText(location) : void 0;
949
- const contactContext = extractContactContext(msg.message ?? void 0);
950
- const externalAdReplyContext = extractExternalAdReplyContext(msg.message ?? void 0);
951
- let mediaKind = extractMediaKind(msg.message ?? void 0);
952
- let body = extractText(msg.message ?? void 0);
819
+ const contactContext = extractContactContext(messageProjection);
820
+ const externalAdReplyContext = extractExternalAdReplyContext(messageProjection);
821
+ let mediaKind = extractMediaKind(messageProjection);
822
+ let body = extractText(messageProjection);
953
823
  if (locationText) body = [body, locationText].filter(Boolean).join("\n").trim();
954
824
  if (!body && !mediaKind) return null;
955
825
  body = body ?? "";
956
826
  const commandBody = body;
957
- const replyContext = describeReplyContext(msg.message);
827
+ const replyContext = describeReplyContext(messageProjection);
958
828
  let mediaPath;
959
829
  let mediaType = mediaKind ? resolveInboundMediaMimetype(msg.message) : void 0;
960
830
  const nativeMedia = mediaKind ? {
@@ -970,8 +840,15 @@ async function enrichWhatsAppInboundMessage(params) {
970
840
  mediaFileName = inboundMedia.fileName;
971
841
  };
972
842
  try {
973
- await saveInboundMedia(await downloadInboundMedia(msg, sock, maxBytes));
843
+ await saveInboundMedia(await downloadInboundMedia(msg, sock, maxBytes, messageProjection[0]));
974
844
  } catch (error) {
845
+ logMediaMaterializationFailure({
846
+ messageId: msg.key?.id,
847
+ mediaKind,
848
+ mimeType: mediaType,
849
+ failureStage: "direct",
850
+ error
851
+ });
975
852
  params.logVerbose(`Inbound media download failed: ${String(error)}`);
976
853
  body = formatInboundMediaUnavailableText({
977
854
  body,
@@ -983,6 +860,13 @@ async function enrichWhatsAppInboundMessage(params) {
983
860
  mediaKind = replyContext.media.kind ?? void 0;
984
861
  mediaType = mediaType ?? replyContext.media.contentType ?? void 0;
985
862
  } catch (error) {
863
+ logMediaMaterializationFailure({
864
+ messageId: msg.key?.id,
865
+ mediaKind: replyContext.media.kind ?? void 0,
866
+ mimeType: replyContext.media.contentType ?? void 0,
867
+ failureStage: "quoted",
868
+ error
869
+ });
986
870
  params.logVerbose(`Quoted media download failed: ${String(error)}`);
987
871
  body = formatInboundMediaUnavailableText({
988
872
  body,
@@ -1000,7 +884,114 @@ async function enrichWhatsAppInboundMessage(params) {
1000
884
  mediaType,
1001
885
  mediaFileName,
1002
886
  mediaKind,
1003
- nativeMedia
887
+ nativeMedia,
888
+ mentionedJids: extractMentionedJids(messageProjection)
889
+ };
890
+ }
891
+ //#endregion
892
+ //#region extensions/whatsapp/src/inbound/access-control.ts
893
+ const PAIRING_REPLY_HISTORY_GRACE_MS = 3e4;
894
+ function logWhatsAppVerbose$2(enabled, message) {
895
+ if (!enabled) return;
896
+ defaultRuntime.log(message);
897
+ }
898
+ function blockedInboundAccess(policy) {
899
+ return {
900
+ allowed: false,
901
+ shouldMarkRead: false,
902
+ isSelfChat: policy.isSelfChat,
903
+ resolvedAccountId: policy.account.accountId
904
+ };
905
+ }
906
+ async function checkInboundAccessControl(params) {
907
+ const policy = resolveWhatsAppInboundPolicy({
908
+ cfg: params.cfg,
909
+ accountId: params.accountId,
910
+ selfE164: params.selfE164
911
+ });
912
+ const pairingGraceMs = typeof params.pairingGraceMs === "number" && params.pairingGraceMs > 0 ? params.pairingGraceMs : PAIRING_REPLY_HISTORY_GRACE_MS;
913
+ const suppressPairingReply = typeof params.connectedAtMs === "number" && typeof params.messageTimestampMs === "number" && params.messageTimestampMs < params.connectedAtMs - pairingGraceMs;
914
+ warnMissingProviderGroupPolicyFallbackOnce({
915
+ providerMissingFallbackApplied: policy.providerMissingFallbackApplied,
916
+ providerKey: "whatsapp",
917
+ accountId: policy.account.accountId,
918
+ log: (message) => logWhatsAppVerbose$2(params.verbose, message)
919
+ });
920
+ const conversationId = params.group ? params.remoteJid : params.from;
921
+ const accessSenderId = params.group ? params.senderE164 : params.from;
922
+ const admissionSenderId = params.group ? params.senderE164 ?? params.senderJid ?? params.from : params.from;
923
+ const resolveChannelIngress = async (contextBinding) => await resolveWhatsAppIngressAccess({
924
+ cfg: params.cfg,
925
+ policy,
926
+ isGroup: params.group,
927
+ conversationId,
928
+ senderId: accessSenderId,
929
+ contextBinding
930
+ });
931
+ const access = await resolveChannelIngress();
932
+ const { senderAccess } = access;
933
+ if (params.group && senderAccess.decision !== "allow") {
934
+ if (senderAccess.reasonCode === "group_policy_disabled") logWhatsAppVerbose$2(params.verbose, "Blocked group message (groupPolicy: disabled)");
935
+ else if (senderAccess.reasonCode === "group_policy_empty_allowlist") logWhatsAppVerbose$2(params.verbose, "Blocked group message (groupPolicy: allowlist, no groupAllowFrom)");
936
+ else logWhatsAppVerbose$2(params.verbose, `Blocked group message from ${params.senderE164 ?? "unknown sender"} (groupPolicy: allowlist)`);
937
+ return blockedInboundAccess(policy);
938
+ }
939
+ if (!params.group) {
940
+ if (params.isFromMe && (policy.account.selfChatMode === false || !policy.isSamePhone(params.from))) {
941
+ logWhatsAppVerbose$2(params.verbose, "Skipping outbound DM (fromMe); no pairing reply needed.");
942
+ return blockedInboundAccess(policy);
943
+ }
944
+ if (senderAccess.decision === "block" && senderAccess.reasonCode === "dm_policy_disabled") {
945
+ logWhatsAppVerbose$2(params.verbose, "Blocked dm (dmPolicy: disabled)");
946
+ return blockedInboundAccess(policy);
947
+ }
948
+ if (senderAccess.decision === "pairing" && !policy.isSamePhone(params.from)) {
949
+ const candidate = params.from;
950
+ if (suppressPairingReply) logWhatsAppVerbose$2(params.verbose, `Skipping pairing reply for historical DM from ${candidate}.`);
951
+ else await createChannelPairingChallengeIssuer({
952
+ channel: "whatsapp",
953
+ accountId: policy.account.accountId,
954
+ upsertPairingRequest: async ({ id, meta }) => await upsertChannelPairingRequest({
955
+ channel: "whatsapp",
956
+ id,
957
+ accountId: policy.account.accountId,
958
+ meta
959
+ })
960
+ })({
961
+ senderId: candidate,
962
+ senderIdLine: `Your WhatsApp phone number: ${candidate}`,
963
+ meta: { name: (params.pushName ?? "").trim() || void 0 },
964
+ onCreated: () => {
965
+ logWhatsAppVerbose$2(params.verbose, `whatsapp pairing request sender=${candidate} name=${params.pushName ?? "unknown"}`);
966
+ },
967
+ sendPairingReply: async (text) => {
968
+ await params.sock.sendMessage(params.remoteJid, { text });
969
+ },
970
+ onReplyError: (err) => {
971
+ logWhatsAppVerbose$2(params.verbose, `whatsapp pairing reply failed for ${candidate}: ${String(err)}`);
972
+ }
973
+ });
974
+ return blockedInboundAccess(policy);
975
+ }
976
+ if (senderAccess.decision !== "allow") {
977
+ logWhatsAppVerbose$2(params.verbose, `Blocked unauthorized sender ${params.from} (dmPolicy=${policy.dmPolicy})`);
978
+ return blockedInboundAccess(policy);
979
+ }
980
+ }
981
+ return {
982
+ allowed: true,
983
+ shouldMarkRead: true,
984
+ isSelfChat: policy.isSelfChat,
985
+ resolvedAccountId: policy.account.accountId,
986
+ admission: buildWhatsAppInboundAdmission({
987
+ policy,
988
+ access,
989
+ channelIngress: access,
990
+ resolveChannelIngress,
991
+ isGroup: params.group,
992
+ conversationId,
993
+ senderId: admissionSenderId
994
+ })
1004
995
  };
1005
996
  }
1006
997
  //#endregion
@@ -1011,7 +1002,14 @@ const recentOutboundMessages = createDedupeCache({
1011
1002
  });
1012
1003
  function buildMessageKey(params) {
1013
1004
  const accountId = params.accountId.trim();
1014
- const remoteJid = params.remoteJid.trim();
1005
+ const rawRemoteJid = params.remoteJid.trim();
1006
+ const hostedPhone = isHostedPnUser(rawRemoteJid);
1007
+ let remoteJid;
1008
+ if (hostedPhone || isHostedLidUser(rawRemoteJid)) {
1009
+ const decodedJid = jidDecode(rawRemoteJid);
1010
+ if (!decodedJid) return null;
1011
+ remoteJid = jidEncode(decodedJid.user, hostedPhone ? "s.whatsapp.net" : "lid");
1012
+ } else remoteJid = jidNormalizedUser(rawRemoteJid);
1015
1013
  const messageId = params.messageId.trim();
1016
1014
  if (!accountId || !remoteJid || !messageId || messageId === "unknown") return null;
1017
1015
  return `${accountId}:${remoteJid}:${messageId}`;
@@ -1027,7 +1025,13 @@ function rememberRecentOutboundMessage(params) {
1027
1025
  function isRecentOutboundMessage(params) {
1028
1026
  const key = buildMessageKey(params);
1029
1027
  if (!key) return false;
1030
- return recentOutboundMessages.peek(key);
1028
+ if (recentOutboundMessages.peek(key)) return true;
1029
+ if (!params.alternateRemoteJid || isJidGroup(params.remoteJid)) return false;
1030
+ const alternateKey = buildMessageKey({
1031
+ ...params,
1032
+ remoteJid: params.alternateRemoteJid
1033
+ });
1034
+ return alternateKey !== null && recentOutboundMessages.peek(alternateKey);
1031
1035
  }
1032
1036
  //#endregion
1033
1037
  //#region extensions/whatsapp/src/inbound/message-normalization.ts
@@ -1039,6 +1043,7 @@ function createWhatsAppInboundMessageNormalizer(options) {
1039
1043
  if (!msg.key?.fromMe || !id || !remoteJid || !isRecentOutboundMessage({
1040
1044
  accountId: options.accountId,
1041
1045
  remoteJid,
1046
+ alternateRemoteJid: msg.key?.remoteJidAlt,
1042
1047
  messageId: id
1043
1048
  })) return false;
1044
1049
  options.logVerbose(`Skipping recent outbound WhatsApp echo ${id} for ${remoteJid}`);
@@ -1048,7 +1053,7 @@ function createWhatsAppInboundMessageNormalizer(options) {
1048
1053
  const id = msg.key?.id ?? void 0;
1049
1054
  const remoteJid = msg.key?.remoteJid;
1050
1055
  if (!remoteJid || remoteJid.endsWith("@status") || remoteJid.endsWith("@broadcast")) return null;
1051
- const group = isJidGroup(remoteJid) === true;
1056
+ const group = isJidGroup$1(remoteJid) === true;
1052
1057
  if (shouldSkipRecentOutboundEcho(msg)) return null;
1053
1058
  if (!hasInboundUserContent(msg.message ?? void 0)) return null;
1054
1059
  const participantJid = msg.key?.participant ?? void 0;
@@ -1122,7 +1127,7 @@ function recordAcceptedInboundActivity(accountId) {
1122
1127
  }
1123
1128
  function createWhatsAppMessageDeliveryCoordinator(options) {
1124
1129
  const inboundLogger = getChildLogger({ module: "web-inbound" });
1125
- const inboundConsoleLog = createSubsystemLogger("gateway/channels/whatsapp").child("inbound");
1130
+ const inboundConsoleLog = createSubsystemLogger$1("gateway/channels/whatsapp").child("inbound");
1126
1131
  const sock = options.sock;
1127
1132
  const socketSession = options.socketSession;
1128
1133
  const groupMetadata = options.groupMetadata;
@@ -1213,7 +1218,7 @@ function createWhatsAppMessageDeliveryCoordinator(options) {
1213
1218
  return normalizeWhatsAppSendResult(await sendTrackedMessage(chatJid, await groupMetadata.applyOutboundMentions(chatJid, previewPayload), optionsValue), "media");
1214
1219
  };
1215
1220
  const timestamp = inbound.messageTimestampMs;
1216
- const mentionedJids = extractMentionedJids(msg.message);
1221
+ const mentionedJids = enriched.mentionedJids;
1217
1222
  const senderName = msg.pushName ?? void 0;
1218
1223
  inboundLogger.info({
1219
1224
  from: inbound.from,
@@ -1256,7 +1261,7 @@ function createWhatsAppMessageDeliveryCoordinator(options) {
1256
1261
  payload: enriched.externalAdReplyContext
1257
1262
  }] : []
1258
1263
  ];
1259
- const inboundMessage = withDeprecatedWebInboundMessageFlatAliases({
1264
+ const inboundMessage = {
1260
1265
  admission: inbound.access.admission,
1261
1266
  event: {
1262
1267
  id: inbound.id,
@@ -1305,7 +1310,7 @@ function createWhatsAppMessageDeliveryCoordinator(options) {
1305
1310
  turnAdoptionLifecycle: durable.turnAdoptionLifecycle,
1306
1311
  readReceipt: durable.readReceipt,
1307
1312
  receiveOrder: durable.receiveOrder
1308
- });
1313
+ };
1309
1314
  if (inboundMessage.event.id) {
1310
1315
  const admission = requireWhatsAppInboundAdmission(inboundMessage);
1311
1316
  cacheInboundMessageMeta(admission.accountId, inboundMessage.platform.chatJid, inboundMessage.event.id, {
@@ -1634,7 +1639,10 @@ async function createWhatsAppAttachedSocketSession(options) {
1634
1639
  if (readinessOptions?.useVerifiedReady && consumeVerifiedSendReady(jid, currentSock)) return;
1635
1640
  const state = getActiveReachoutTimelock(reachoutTimeLock) ?? await fetchReachoutTimeLock(currentSock);
1636
1641
  const activeState = getActiveReachoutTimelock(state);
1637
- if (activeState) throw new Error(formatReachoutTimelockError(activeState));
1642
+ if (activeState) {
1643
+ const error = new Error(formatReachoutTimelockError(activeState));
1644
+ throw readinessOptions?.rememberReady ? new PlatformMessageNotDispatchedError(error.message, { cause: error }) : error;
1645
+ }
1638
1646
  if (readinessOptions?.rememberReady && state) rememberVerifiedSendReady(jid, currentSock);
1639
1647
  };
1640
1648
  const assertSendReady = async (to) => {
@@ -1757,7 +1765,7 @@ function logWhatsAppVerbose(enabled, message) {
1757
1765
  }
1758
1766
  async function attachWebInboxToSocket(options) {
1759
1767
  const inboundLogger = getChildLogger({ module: "web-inbound" });
1760
- const inboundConsoleLog = createSubsystemLogger("gateway/channels/whatsapp").child("inbound");
1768
+ const inboundConsoleLog = createSubsystemLogger$1("gateway/channels/whatsapp").child("inbound");
1761
1769
  const socketSession = await createWhatsAppAttachedSocketSession({
1762
1770
  sock: options.sock,
1763
1771
  socketRef: options.socketRef,
@@ -1856,10 +1864,6 @@ async function monitorWebInbox(options) {
1856
1864
  }
1857
1865
  return attachWebInboxToSocket({
1858
1866
  ...options,
1859
- onMessage: async (msg) => {
1860
- await options.onMessage(normalizeAdmittedWebInboundMessage(msg));
1861
- },
1862
- shouldDebounce: options.shouldDebounce ? (msg) => options.shouldDebounce?.(normalizeAdmittedWebInboundMessage(msg)) ?? true : void 0,
1863
1867
  socketTiming,
1864
1868
  sock,
1865
1869
  recentMessageKeys,
@@ -1868,7 +1872,7 @@ async function monitorWebInbox(options) {
1868
1872
  }
1869
1873
  //#endregion
1870
1874
  //#region extensions/whatsapp/src/auto-reply/loggers.ts
1871
- const whatsappLog = createSubsystemLogger("gateway/channels/whatsapp");
1875
+ const whatsappLog = createSubsystemLogger$1("gateway/channels/whatsapp");
1872
1876
  const whatsappInboundLog = whatsappLog.child("inbound");
1873
1877
  const whatsappOutboundLog = whatsappLog.child("outbound");
1874
1878
  const whatsappHeartbeatLog = whatsappLog.child("heartbeat");
@@ -1932,6 +1936,15 @@ function resolveOwnerList(mentionCfg, selfE164) {
1932
1936
  }
1933
1937
  //#endregion
1934
1938
  //#region extensions/whatsapp/src/auto-reply/monitor-state.ts
1939
+ const LIFECYCLE_BY_HEALTH_STATE = {
1940
+ starting: "starting",
1941
+ healthy: "ready",
1942
+ stale: "recovering",
1943
+ reconnecting: "recovering",
1944
+ conflict: "blocked",
1945
+ "logged-out": "blocked",
1946
+ stopped: "blocked"
1947
+ };
1935
1948
  function cloneStatus(status) {
1936
1949
  return {
1937
1950
  ...status,
@@ -1955,7 +1968,8 @@ function createWebChannelStatusController(statusSink) {
1955
1968
  lastError: null,
1956
1969
  busy: false,
1957
1970
  lastRunActivityAt: null,
1958
- healthState: "starting"
1971
+ healthState: "starting",
1972
+ lifecycle: "starting"
1959
1973
  };
1960
1974
  const emit = () => {
1961
1975
  statusSink?.(cloneStatus(status));
@@ -1964,16 +1978,17 @@ function createWebChannelStatusController(statusSink) {
1964
1978
  emit,
1965
1979
  snapshot: () => status,
1966
1980
  noteConnected(at = Date.now()) {
1967
- Object.assign(status, createConnectedChannelStatusPatch(at));
1981
+ Object.assign(status, channelReadyPatch({
1982
+ lastConnectedAt: at,
1983
+ lastEventAt: at
1984
+ }));
1968
1985
  Object.assign(status, createTransportActivityStatusPatch(at));
1969
1986
  if (lastDisconnectWasWatchdogRecovery) {
1970
1987
  status.lastDisconnect = null;
1971
1988
  status.reconnectAttempts = 0;
1972
1989
  lastDisconnectWasWatchdogRecovery = false;
1973
1990
  }
1974
- status.lastError = null;
1975
1991
  status.healthState = "healthy";
1976
- status.terminalDisconnect = void 0;
1977
1992
  emit();
1978
1993
  },
1979
1994
  noteInbound(at = Date.now()) {
@@ -1981,7 +1996,10 @@ function createWebChannelStatusController(statusSink) {
1981
1996
  status.lastMessageAt = at;
1982
1997
  status.lastEventAt = at;
1983
1998
  Object.assign(status, createTransportActivityStatusPatch(at));
1984
- if (status.connected) status.healthState = "healthy";
1999
+ if (status.connected) {
2000
+ status.healthState = "healthy";
2001
+ status.lifecycle = "ready";
2002
+ }
1985
2003
  emit();
1986
2004
  },
1987
2005
  noteTransportActivity(at = Date.now()) {
@@ -1993,12 +2011,18 @@ function createWebChannelStatusController(statusSink) {
1993
2011
  if (status.busy === busy && status.lastRunActivityAt === at) return;
1994
2012
  status.busy = busy;
1995
2013
  status.lastRunActivityAt = at;
1996
- if (status.connected && busy) status.healthState = "healthy";
2014
+ if (status.connected && busy) {
2015
+ status.healthState = "healthy";
2016
+ status.lifecycle = "ready";
2017
+ }
1997
2018
  emit();
1998
2019
  },
1999
2020
  noteWatchdogStale(at = Date.now()) {
2000
2021
  status.lastEventAt = at;
2001
- if (status.connected) status.healthState = "stale";
2022
+ if (status.connected) {
2023
+ status.healthState = "stale";
2024
+ status.lifecycle = "recovering";
2025
+ }
2002
2026
  emit();
2003
2027
  },
2004
2028
  noteReconnectAttempts(reconnectAttempts) {
@@ -2019,52 +2043,28 @@ function createWebChannelStatusController(statusSink) {
2019
2043
  status.lastError = params.error ?? null;
2020
2044
  status.reconnectAttempts = params.reconnectAttempts;
2021
2045
  status.healthState = params.healthState;
2046
+ status.lifecycle = LIFECYCLE_BY_HEALTH_STATE[params.healthState];
2022
2047
  emit();
2023
2048
  },
2024
2049
  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";
2050
+ const terminalDisconnect = status.lifecycle === "blocked";
2051
+ if (!isTerminalHealthState(status.healthState)) {
2052
+ Object.assign(status, channelStoppedPatch({
2053
+ lastEventAt: at,
2054
+ terminalDisconnect
2055
+ }));
2056
+ status.healthState = "stopped";
2057
+ } else Object.assign(status, {
2058
+ running: false,
2059
+ connected: false,
2060
+ lastEventAt: at,
2061
+ terminalDisconnect
2062
+ });
2030
2063
  emit();
2031
2064
  }
2032
2065
  };
2033
2066
  }
2034
2067
  //#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
2068
  //#region extensions/whatsapp/src/auto-reply/monitor/listener-log.ts
2069
2069
  function formatWhatsAppInboundListeningLog(account) {
2070
2070
  if (account.groupPolicy === "disabled") return "Listening for WhatsApp inbound messages (DM + groups disabled by groupPolicy).";
@@ -2157,8 +2157,6 @@ async function resolveWhatsAppReactionEligibility(params) {
2157
2157
  agentId: params.agentId,
2158
2158
  ackConfig: params.cfg.messages?.ackReaction
2159
2159
  });
2160
- const directEnabled = scope === "all" || scope === "direct";
2161
- const groupMode = scope === "all" || scope === "group-all" ? "always" : scope === "direct" ? "never" : "mentions";
2162
2160
  const isGroup = admission.conversation.kind === "group";
2163
2161
  const activation = isGroup ? await resolveGroupActivationFor({
2164
2162
  cfg: params.cfg,
@@ -2167,14 +2165,14 @@ async function resolveWhatsAppReactionEligibility(params) {
2167
2165
  sessionKey: params.sessionKey,
2168
2166
  conversationId: admission.conversation.id
2169
2167
  }) : null;
2170
- if (!shouldAckReactionForWhatsApp({
2171
- emoji,
2168
+ if (!emoji || !shouldAckReaction({
2169
+ scope,
2172
2170
  isDirect: admission.conversation.kind === "direct",
2173
2171
  isGroup,
2174
- directEnabled,
2175
- groupMode,
2176
- wasMentioned: (params.msg.groupMention?.wasMentioned ?? params.msg.wasMentioned) === true,
2177
- groupActivated: activation === "always"
2172
+ isMentionableGroup: isGroup,
2173
+ canDetectMention: isGroup,
2174
+ effectiveWasMentioned: (params.msg.groupMention?.wasMentioned ?? params.msg.wasMentioned) === true,
2175
+ shouldBypassMention: activation === "always"
2178
2176
  })) return DISABLED_REACTION;
2179
2177
  const participant = resolveReactionParticipant(params.msg);
2180
2178
  return {
@@ -2184,7 +2182,7 @@ async function resolveWhatsAppReactionEligibility(params) {
2184
2182
  emoji,
2185
2183
  reactionOptions: {
2186
2184
  verbose: params.verbose,
2187
- fromMe: false,
2185
+ fromMe: params.msg.platform.fromMe === true,
2188
2186
  ...participant ? { participant } : {},
2189
2187
  accountId,
2190
2188
  cfg: params.cfg
@@ -2375,7 +2373,7 @@ function recordPendingGroupHistoryEntry(params) {
2375
2373
  timestamp: params.msg.event.timestamp,
2376
2374
  id: params.msg.event.id,
2377
2375
  senderJid: senderIdentity.jid ?? params.msg.platform.senderJid,
2378
- ...params.body === void 0 && params.msg.payload.media ? { media: [{
2376
+ ...params.msg.payload.media ? { media: [{
2379
2377
  path: params.msg.payload.media.path,
2380
2378
  url: params.msg.payload.media.url ?? params.msg.payload.media.path,
2381
2379
  contentType: params.msg.payload.media.type,
@@ -2495,7 +2493,8 @@ async function applyGroupGating(params) {
2495
2493
  needsMentionText: true
2496
2494
  };
2497
2495
  }
2498
- return skipGroupMessageAndStoreHistory(params, `Group message stored for context (no mention detected) in ${conversationId}: ${mentionMsg.payload.body}`, params.mentionText);
2496
+ const pendingHistoryBody = params.mentionText === void 0 ? void 0 : formatAudioTranscriptForAgent(params.mentionText);
2497
+ return skipGroupMessageAndStoreHistory(params, `Group message stored for context (no mention detected) in ${conversationId}: ${mentionMsg.payload.body}`, pendingHistoryBody);
2499
2498
  }
2500
2499
  return { shouldProcess: true };
2501
2500
  }
@@ -2571,7 +2570,7 @@ var WhatsAppOutboundRetryError = class extends Error {
2571
2570
  }
2572
2571
  };
2573
2572
  function isRetryableWhatsAppOutboundError(error) {
2574
- if (isWhatsAppSocketOperationTimeoutError(error)) return false;
2573
+ if (isChannelPartialDeliveryError(error) || isWhatsAppSocketOperationTimeoutError(error)) return false;
2575
2574
  return WHATSAPP_RETRYABLE_OUTBOUND_ERROR_PATTERN.test(formatError(error));
2576
2575
  }
2577
2576
  async function sendWhatsAppOutboundWithRetry(params) {
@@ -2706,14 +2705,14 @@ function createWhatsAppReplyDeliveryReceipt(results) {
2706
2705
  });
2707
2706
  }
2708
2707
  async function deliverWebReply(params) {
2708
+ return await withWhatsAppLogicalDeliveryActivity(() => deliverWebReplyInActivityScope(params));
2709
+ }
2710
+ async function deliverWebReplyInActivityScope(params) {
2709
2711
  const { replyResult, transport, maxMediaBytes, textLimit, replyLogger, connectionId, skipLog } = params;
2710
2712
  const conversationId = transport.conversationId;
2711
2713
  const isGroupConversation = transport.conversationKind === "group";
2712
2714
  const replyStarted = Date.now();
2713
2715
  const sendResults = [];
2714
- const rememberSendResult = (result) => {
2715
- if (result) sendResults.push(result);
2716
- };
2717
2716
  const finishDelivery = () => {
2718
2717
  const receipt = createWhatsAppReplyDeliveryReceipt(sendResults);
2719
2718
  return {
@@ -2722,6 +2721,32 @@ async function deliverWebReply(params) {
2722
2721
  providerAccepted: sendResults.some((result) => result.providerAccepted)
2723
2722
  };
2724
2723
  };
2724
+ const preserveAcceptedDeliveryError = (error, kind) => {
2725
+ const sendKind = kind ?? sendResults[0]?.kind ?? "text";
2726
+ if (isChannelPartialDeliveryError(error)) rememberWhatsAppPartialSend({
2727
+ error,
2728
+ kind: sendKind,
2729
+ results: sendResults
2730
+ });
2731
+ return mergeWhatsAppAcceptedSendError({
2732
+ error,
2733
+ kind: sendKind,
2734
+ results: sendResults
2735
+ });
2736
+ };
2737
+ const rememberSendResult = (result) => {
2738
+ if (!result) return;
2739
+ try {
2740
+ rememberWhatsAppAcceptedSend({
2741
+ accountId: transport.accountId,
2742
+ result,
2743
+ results: sendResults
2744
+ });
2745
+ } catch (error) {
2746
+ if (sendResults.some((accepted) => accepted.providerAccepted)) throw preserveAcceptedDeliveryError(error, result.kind);
2747
+ throw error;
2748
+ }
2749
+ };
2725
2750
  if (isReasoningReplyPayload(replyResult)) {
2726
2751
  whatsappOutboundLog.debug(`Suppressed reasoning payload to ${conversationId}`);
2727
2752
  return finishDelivery();
@@ -2743,7 +2768,7 @@ async function deliverWebReply(params) {
2743
2768
  media: cached?.media
2744
2769
  });
2745
2770
  };
2746
- const sendWithRetry = async (fn, label) => {
2771
+ const sendWithRetry = async (fn, label, kind) => {
2747
2772
  try {
2748
2773
  return await sendWhatsAppOutboundWithRetry({
2749
2774
  send: fn,
@@ -2752,7 +2777,7 @@ async function deliverWebReply(params) {
2752
2777
  }
2753
2778
  });
2754
2779
  } catch (error) {
2755
- if (sendResults.some((result) => result.providerAccepted)) throw markWhatsAppVisibleDeliveryError(error);
2780
+ if (isChannelPartialDeliveryError(error) || sendResults.some((result) => result.providerAccepted)) throw preserveAcceptedDeliveryError(error, kind);
2756
2781
  throw error;
2757
2782
  }
2758
2783
  };
@@ -2761,7 +2786,7 @@ async function deliverWebReply(params) {
2761
2786
  for (const [index, chunk] of textChunks.entries()) {
2762
2787
  const chunkStarted = Date.now();
2763
2788
  const quote = getQuote();
2764
- rememberSendResult(await sendWithRetry(() => transport.reply(chunk, quote), "text"));
2789
+ rememberSendResult(await sendWithRetry(() => transport.reply(chunk, quote), "text", "text"));
2765
2790
  if (!skipLog) {
2766
2791
  const durationMs = Date.now() - chunkStarted;
2767
2792
  whatsappOutboundLog.debug(`Sent chunk ${index + 1}/${totalChunks} to ${conversationId} (${durationMs.toFixed(0)}ms)`);
@@ -2784,10 +2809,13 @@ async function deliverWebReply(params) {
2784
2809
  return delivery;
2785
2810
  }
2786
2811
  const remainingText = [...textChunks];
2812
+ const leadingCaption = remainingText.shift() || "";
2813
+ let acceptedIdsBeforeCurrentMedia = /* @__PURE__ */ new Set();
2787
2814
  await sendMediaWithLeadingCaption({
2788
2815
  mediaUrls: mediaList,
2789
- caption: remainingText.shift() || "",
2816
+ caption: leadingCaption,
2790
2817
  send: async ({ mediaUrl, caption }) => {
2818
+ acceptedIdsBeforeCurrentMedia = new Set(sendResults.flatMap(listWhatsAppSendResultMessageIds));
2791
2819
  const media = await prepareWhatsAppOutboundMedia(await loadWebMedia(mediaUrl, {
2792
2820
  maxBytes: maxMediaBytes,
2793
2821
  localRoots: params.mediaLocalRoots
@@ -2802,22 +2830,22 @@ async function deliverWebReply(params) {
2802
2830
  image: media.buffer,
2803
2831
  caption,
2804
2832
  mimetype: media.mimetype
2805
- }, quote), "media:image"));
2833
+ }, quote), "media:image", "media"));
2806
2834
  } else if (media.kind === "audio") {
2807
2835
  const quote = getQuote();
2808
2836
  rememberSendResult(await sendWithRetry(() => transport.sendMedia({
2809
2837
  audio: media.buffer,
2810
2838
  ptt: true,
2811
2839
  mimetype: media.mimetype
2812
- }, quote), "media:audio"));
2813
- if (caption) rememberSendResult(await sendWithRetry(() => transport.reply(caption, quote), "media:audio-text"));
2840
+ }, quote), "media:audio", "media"));
2841
+ if (caption) rememberSendResult(await sendWithRetry(() => transport.reply(caption, quote), "media:audio-text", "text"));
2814
2842
  } else if (media.kind === "video") {
2815
2843
  const quote = getQuote();
2816
2844
  rememberSendResult(await sendWithRetry(() => transport.sendMedia({
2817
2845
  video: media.buffer,
2818
2846
  caption,
2819
2847
  mimetype: media.mimetype
2820
- }, quote), "media:video"));
2848
+ }, quote), "media:video", "media"));
2821
2849
  } else {
2822
2850
  const quote = getQuote();
2823
2851
  rememberSendResult(await sendWithRetry(() => transport.sendMedia({
@@ -2825,7 +2853,7 @@ async function deliverWebReply(params) {
2825
2853
  fileName: media.fileName,
2826
2854
  caption,
2827
2855
  mimetype: media.mimetype
2828
- }, quote), "media:document"));
2856
+ }, quote), "media:document", "media"));
2829
2857
  }
2830
2858
  whatsappOutboundLog.info(`Sent media reply to ${conversationId} (${(media.buffer.length / (1024 * 1024)).toFixed(2)}MB)`);
2831
2859
  replyLogger.info({
@@ -2841,6 +2869,7 @@ async function deliverWebReply(params) {
2841
2869
  }, "auto-reply sent (media)");
2842
2870
  },
2843
2871
  onError: async ({ error, mediaUrl, caption, isFirst }) => {
2872
+ if (sendResults.some((result) => listWhatsAppSendResultMessageIds(result).some((messageId) => !acceptedIdsBeforeCurrentMedia.has(messageId)))) throw preserveAcceptedDeliveryError(error);
2844
2873
  whatsappOutboundLog.error(`Failed sending web media to ${conversationId}: ${formatError(error)}`);
2845
2874
  replyLogger.warn({
2846
2875
  err: error,
@@ -2848,16 +2877,16 @@ async function deliverWebReply(params) {
2848
2877
  }, "failed to send web media reply");
2849
2878
  if (!isFirst) {
2850
2879
  whatsappOutboundLog.warn(`Trailing media failed; sent warning to ${conversationId}`);
2851
- rememberSendResult(await sendWithRetry(() => transport.reply("⚠️ Media unavailable.", getQuote()), "media:fallback-unavailable"));
2880
+ rememberSendResult(await sendWithRetry(() => transport.reply("⚠️ Media unavailable.", getQuote()), "media:fallback-unavailable", "text"));
2852
2881
  return;
2853
2882
  }
2854
2883
  const fallbackText = [caption ?? "", "⚠️ Media failed."].filter(Boolean).join("\n");
2855
2884
  if (!fallbackText) return;
2856
2885
  whatsappOutboundLog.warn(`Media skipped; sent text-only to ${conversationId}`);
2857
- rememberSendResult(await sendWithRetry(() => transport.reply(fallbackText, getQuote()), "media:fallback-text"));
2886
+ rememberSendResult(await sendWithRetry(() => transport.reply(fallbackText, getQuote()), "media:fallback-text", "text"));
2858
2887
  }
2859
2888
  });
2860
- for (const chunk of remainingText) rememberSendResult(await sendWithRetry(() => transport.reply(chunk, getQuote()), "media:text"));
2889
+ for (const chunk of remainingText) rememberSendResult(await sendWithRetry(() => transport.reply(chunk, getQuote()), "media:text", "text"));
2861
2890
  return finishDelivery();
2862
2891
  }
2863
2892
  //#endregion
@@ -2941,7 +2970,7 @@ function projectPreparedChannelInbound(params) {
2941
2970
  textForCommands: inbound.message.commandBody,
2942
2971
  raw: inbound
2943
2972
  },
2944
- context: buildChannelInboundEventContext({
2973
+ context: params.buildContext({
2945
2974
  ...inbound,
2946
2975
  messageId: inbound.event.id,
2947
2976
  messageIdFull: inbound.event.fullId,
@@ -3003,7 +3032,7 @@ function isWhatsAppVisibleDeliveryError(error) {
3003
3032
  return typeof error === "object" && error !== null && !Array.isArray(error) && error.visibleReplySent === true || isChannelPartialDeliveryError(error) && error.deliveryResult.visibleReplySent;
3004
3033
  }
3005
3034
  function readTrimmedString(value) {
3006
- return typeof value === "string" ? value.trim() : "";
3035
+ return normalizeOptionalString(value) ?? "";
3007
3036
  }
3008
3037
  function markWhatsAppReplyDeliveryErrorVisibleAfterFlush(error, flushResult) {
3009
3038
  if (flushResult.delivered === 0) return error;
@@ -3128,6 +3157,13 @@ async function prepareWhatsAppInboundContext(params) {
3128
3157
  const admission = requireWhatsAppInboundAdmission(params.msg);
3129
3158
  const conversationId = admission.conversation.id;
3130
3159
  const conversationKind = admission.conversation.kind;
3160
+ const eventId = params.msg.event.id ?? `${conversationId}:${newConnectionId()}`;
3161
+ const channelIngress = await resolveWhatsAppAdmissionChannelIngress(admission, {
3162
+ agentId: params.route.agentId,
3163
+ sessionKey: params.route.sessionKey,
3164
+ messageId: eventId,
3165
+ inboundEventKind: "user_request"
3166
+ }) ?? admission.channelIngress;
3131
3167
  const wasMentioned = params.msg.groupMention?.wasMentioned ?? params.msg.wasMentioned;
3132
3168
  const inboundHistory = conversationKind === "group" ? buildInboundHistoryFromEntries({
3133
3169
  entries: (params.groupHistory ?? []).map((entry) => ({
@@ -3147,6 +3183,7 @@ async function prepareWhatsAppInboundContext(params) {
3147
3183
  }] : void 0, { transcribed: (_entry, index) => params.mediaTranscribedIndexes?.includes(index) === true });
3148
3184
  const control = { messageReceivedHooks: params.suppressMessageReceivedHooks ? "channel" : "core" };
3149
3185
  const inbound = {
3186
+ channelIngress,
3150
3187
  channel: "whatsapp",
3151
3188
  supplemental: {
3152
3189
  quote: params.visibleReplyTo ? {
@@ -3159,7 +3196,7 @@ async function prepareWhatsAppInboundContext(params) {
3159
3196
  },
3160
3197
  media,
3161
3198
  event: {
3162
- id: params.msg.event.id ?? `${conversationId}:${newConnectionId()}`,
3199
+ id: eventId,
3163
3200
  timestamp: params.msg.event.timestamp
3164
3201
  },
3165
3202
  from: conversationId,
@@ -3212,7 +3249,8 @@ async function prepareWhatsAppInboundContext(params) {
3212
3249
  };
3213
3250
  const projected = projectPreparedChannelInbound({
3214
3251
  inbound,
3215
- control
3252
+ control,
3253
+ buildContext: params.buildContext ?? buildChannelInboundEventContext
3216
3254
  });
3217
3255
  return {
3218
3256
  inbound,
@@ -3271,13 +3309,6 @@ function createWhatsAppReplyPlan(params) {
3271
3309
  let didLogHeartbeatStrip = false;
3272
3310
  const recordDeliveredPayload = (payload) => {
3273
3311
  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
3312
  if (shouldLogVerbose$1()) {
3282
3313
  const reply = resolveSendableOutboundReplyParts(payload);
3283
3314
  logVerbose$1(`Reply body: ${payload.text != null ? reply.text : "<media>"}${reply.hasMedia ? " (media)" : ""} -> ${conversationId}`);
@@ -3324,14 +3355,7 @@ function createWhatsAppReplyPlan(params) {
3324
3355
  }, "auto-reply was not accepted by WhatsApp provider");
3325
3356
  return result;
3326
3357
  }
3327
- if (options?.recordDelivery !== false) try {
3328
- recordDeliveredPayload(normalizedDeliveryPayload);
3329
- } catch (error) {
3330
- throw createChannelPartialDeliveryError(error, {
3331
- ...result,
3332
- visibleReplySent: true
3333
- });
3334
- }
3358
+ if (options?.recordDelivery !== false) recordDeliveredPayload(normalizedDeliveryPayload);
3335
3359
  return result;
3336
3360
  };
3337
3361
  const mediaOnlyCoalescer = createWhatsAppMediaOnlyReplyCoalescer({ deliver: async (pending) => {
@@ -3448,13 +3472,16 @@ function createWhatsAppReplyPlan(params) {
3448
3472
  onToolStart: async (payload) => {
3449
3473
  const toolName = payload.name?.trim();
3450
3474
  if (toolName) await statusReactionController.setTool(toolName);
3475
+ return false;
3451
3476
  },
3452
3477
  onCompactionStart: async () => {
3453
3478
  await statusReactionController.setCompacting();
3479
+ return false;
3454
3480
  },
3455
3481
  onCompactionEnd: async () => {
3456
3482
  statusReactionController.cancelPending();
3457
3483
  await statusReactionController.setThinking();
3484
+ return false;
3458
3485
  }
3459
3486
  } : {}
3460
3487
  },
@@ -3473,7 +3500,7 @@ function createWhatsAppReplyPlan(params) {
3473
3500
  }
3474
3501
  if (statusReactionController) finalizeWhatsAppStatusReaction({
3475
3502
  controller: statusReactionController,
3476
- outcome: didDeliverVisibleReply ? "done" : "error"
3503
+ outcome: readAgentRunTerminalOutcome(dispatchResult) === "failed" || !didDeliverVisibleReply ? "error" : "done"
3477
3504
  });
3478
3505
  if (params.shouldClearGroupHistory) params.groupHistories.set(params.groupHistoryKey, []);
3479
3506
  return didDeliverVisibleReply;
@@ -3487,12 +3514,8 @@ async function finalizeWhatsAppStatusReaction(params) {
3487
3514
  }
3488
3515
  //#endregion
3489
3516
  //#region extensions/whatsapp/src/auto-reply/monitor/message-line.runtime.ts
3490
- function normalizeAgentId$1(agentId) {
3491
- return agentId.trim().toLowerCase() || "main";
3492
- }
3493
3517
  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();
3518
+ const identityName = resolveAgentConfig(cfg, agentId)?.identity?.name?.trim();
3496
3519
  return identityName ? `[${identityName}]` : void 0;
3497
3520
  }
3498
3521
  function resolveMessagePrefix(cfg, agentId, opts) {
@@ -3660,7 +3683,7 @@ async function processMessage(params) {
3660
3683
  ...params.msg,
3661
3684
  payload: {
3662
3685
  ...params.msg.payload,
3663
- body: audioTranscript
3686
+ body: formatAudioTranscriptForAgent(audioTranscript)
3664
3687
  }
3665
3688
  } : params.msg;
3666
3689
  const visibleReplyTo = resolveVisibleWhatsAppReplyContext({
@@ -3711,15 +3734,6 @@ async function processMessage(params) {
3711
3734
  });
3712
3735
  shouldClearGroupHistory = !(params.suppressGroupHistoryClear ?? false);
3713
3736
  }
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
3737
  const statusReactionController = params.statusReactionController ?? (params.cfg.messages?.statusReactions?.enabled === true && !params.ackAlreadySent ? await createWhatsAppStatusReactionController({
3724
3738
  cfg: params.cfg,
3725
3739
  msg: params.msg,
@@ -3803,6 +3817,7 @@ async function processMessage(params) {
3803
3817
  msg: params.msg,
3804
3818
  rawBody: commandBody,
3805
3819
  route: params.route,
3820
+ buildContext: params.buildContext,
3806
3821
  sender: {
3807
3822
  id: getPrimaryIdentityId(sender) ?? void 0,
3808
3823
  name: sender.name ?? void 0,
@@ -3877,7 +3892,6 @@ async function processMessage(params) {
3877
3892
  maxMediaTextChunkLimit: params.maxMediaTextChunkLimit,
3878
3893
  inbound,
3879
3894
  onModelSelected,
3880
- rememberSentText: params.rememberSentText,
3881
3895
  replyLogger: params.replyLogger,
3882
3896
  replyPipeline: {
3883
3897
  ...replyPipeline,
@@ -3900,6 +3914,7 @@ async function processMessage(params) {
3900
3914
  sessionKey: params.route.sessionKey
3901
3915
  },
3902
3916
  ctxPayload,
3917
+ dispatchReplyFromConfig: params.dispatchReplyFromConfig,
3903
3918
  record: {
3904
3919
  onRecordError: (err) => {
3905
3920
  params.replyLogger.warn({
@@ -3934,19 +3949,13 @@ async function processMessage(params) {
3934
3949
  }
3935
3950
  //#endregion
3936
3951
  //#region extensions/whatsapp/src/auto-reply/monitor/on-message.ts
3937
- function readDeprecatedAccessControlPassed(msg) {
3938
- return msg.accessControlPassed;
3939
- }
3940
3952
  function createWebOnMessageHandler(params) {
3941
- const hasExplicitlyPassedInboundAccess = (msg) => {
3942
- if (msg.admission.ingress.decisiveGateId === "legacy-flat-compat") return readDeprecatedAccessControlPassed(msg) === true;
3943
- return msg.admission.ingress.decision === "allow";
3944
- };
3953
+ const hasExplicitlyPassedInboundAccess = (msg) => msg.admission.ingress.decision === "allow";
3945
3954
  const withDirectSenderPeer = (msg, peerId) => {
3946
3955
  if (requireWhatsAppInboundAdmission(msg).conversation.kind === "group" || msg.platform.sender?.e164 || msg.platform.senderE164 || !peerId.startsWith("+")) return msg;
3947
3956
  const normalized = normalizeE164(peerId);
3948
3957
  if (!normalized) return msg;
3949
- return requireAdmittedWhatsAppInboundMessage(withDeprecatedWebInboundMessageFlatAliases({
3958
+ return {
3950
3959
  ...msg,
3951
3960
  platform: {
3952
3961
  ...msg.platform,
@@ -3956,7 +3965,7 @@ function createWebOnMessageHandler(params) {
3956
3965
  },
3957
3966
  senderE164: normalized
3958
3967
  }
3959
- }));
3968
+ };
3960
3969
  };
3961
3970
  const processForRoute = async (cfg, msg, route, groupHistoryKey, opts) => {
3962
3971
  const processParams = {
@@ -3973,10 +3982,8 @@ function createWebOnMessageHandler(params) {
3973
3982
  replyResolver: params.replyResolver,
3974
3983
  replyLogger: params.replyLogger,
3975
3984
  backgroundTasks: params.backgroundTasks,
3976
- rememberSentText: params.echoTracker.rememberText,
3977
- echoHas: params.echoTracker.has,
3978
- echoForget: params.echoTracker.forget,
3979
- buildCombinedEchoKey: params.echoTracker.buildCombinedKey
3985
+ buildContext: params.buildContext,
3986
+ dispatchReplyFromConfig: params.dispatchReplyFromConfig
3980
3987
  };
3981
3988
  if (opts?.groupHistory !== void 0) processParams.groupHistory = opts.groupHistory;
3982
3989
  if (opts?.suppressGroupHistoryClear !== void 0) processParams.suppressGroupHistoryClear = opts.suppressGroupHistoryClear;
@@ -4012,11 +4019,6 @@ function createWebOnMessageHandler(params) {
4012
4019
  });
4013
4020
  const baseMentionConfig = buildMentionConfig(cfg);
4014
4021
  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
4022
  const configuredRoute = resolveConfiguredBindingRoute({
4021
4023
  cfg,
4022
4024
  route: baseConversationRoute,
@@ -4292,10 +4294,6 @@ async function monitorWebChannel(verbose, listenerFactory = attachWebInboxToSock
4292
4294
  const groupMetadataCache = /* @__PURE__ */ new Map();
4293
4295
  const recentMessageKeys = /* @__PURE__ */ new Map();
4294
4296
  const baileysGroupMetaCache = /* @__PURE__ */ new Map();
4295
- const echoTracker = createEchoTracker({
4296
- maxItems: 100,
4297
- logVerbose
4298
- });
4299
4297
  const sleep = tuning.sleep ?? ((ms, signal) => sleepWithAbort(ms, signal ?? abortSignal));
4300
4298
  const stopRequested = () => abortSignal?.aborted === true;
4301
4299
  const currentMaxListeners = process.getMaxListeners?.() ?? 10;
@@ -4334,15 +4332,12 @@ async function monitorWebChannel(verbose, listenerFactory = attachWebInboxToSock
4334
4332
  cfg,
4335
4333
  channel: "whatsapp"
4336
4334
  });
4337
- const shouldDebounce = (msg) => {
4338
- const admitted = normalizeAdmittedWebInboundMessage(msg);
4339
- return shouldDebounceTextInbound({
4340
- text: admitted.payload.commandBody ?? admitted.payload.body,
4341
- cfg,
4342
- hasMedia: Boolean(admitted.payload.media?.path || admitted.payload.media?.type),
4343
- allowDebounce: !(admitted.payload.location || admitted.quote?.id || admitted.quote?.body)
4344
- });
4345
- };
4335
+ const shouldDebounce = (msg) => shouldDebounceTextInbound({
4336
+ text: msg.payload.commandBody ?? msg.payload.body,
4337
+ cfg,
4338
+ hasMedia: Boolean(msg.payload.media?.path || msg.payload.media?.type),
4339
+ allowDebounce: !(msg.payload.location || msg.quote?.id || msg.quote?.body)
4340
+ });
4346
4341
  let connection;
4347
4342
  try {
4348
4343
  connection = await controller.openConnection({
@@ -4353,6 +4348,7 @@ async function monitorWebChannel(verbose, listenerFactory = attachWebInboxToSock
4353
4348
  return meta?.participants?.length ? meta : void 0;
4354
4349
  },
4355
4350
  createListener: async ({ sock, connection: connectionLocal }) => {
4351
+ const pluginChannelRuntime = tuning.channelRuntime;
4356
4352
  const onMessage = createWebOnMessageHandler({
4357
4353
  cfg,
4358
4354
  loadConfig: loadCurrentMonitorConfig,
@@ -4362,12 +4358,13 @@ async function monitorWebChannel(verbose, listenerFactory = attachWebInboxToSock
4362
4358
  groupHistoryLimit,
4363
4359
  groupHistories,
4364
4360
  groupMemberNames,
4365
- echoTracker,
4366
4361
  backgroundTasks: connectionLocal.backgroundTasks,
4367
4362
  replyResolver: activeReplyResolver,
4368
4363
  replyLogger,
4369
4364
  baseMentionConfig,
4370
- account
4365
+ account,
4366
+ buildContext: pluginChannelRuntime?.inbound.buildContext,
4367
+ dispatchReplyFromConfig: pluginChannelRuntime?.reply?.dispatchReplyFromConfig
4371
4368
  });
4372
4369
  return await (listenerFactory ?? attachWebInboxToSocket)({
4373
4370
  cfg,
@@ -4394,11 +4391,10 @@ async function monitorWebChannel(verbose, listenerFactory = attachWebInboxToSock
4394
4391
  recentMessageKeys,
4395
4392
  baileysGroupMetaCache,
4396
4393
  onMessage: async (msg) => {
4397
- const admitted = normalizeAdmittedWebInboundMessage(msg);
4398
4394
  const inboundAt = Date.now();
4399
4395
  controller.noteInbound(inboundAt);
4400
4396
  statusController.noteInbound(inboundAt);
4401
- await onMessage(admitted);
4397
+ await onMessage(msg);
4402
4398
  },
4403
4399
  onPendingWorkChanged: (pendingWorkCount, at) => {
4404
4400
  statusController.noteBusy(pendingWorkCount > 0, at);